diff --git a/.dockerignore b/.dockerignore index e4dd50f56..7ceb7a27a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,25 @@ -node_modules -client/node_modules -server/build -server/node_modules \ No newline at end of file +**/node_modules +**/dist +**/build +**/coverage + +.git +.github +.gitignore + +**/.vscode +**/.idea +**/.DS_Store + +**/test +**/tests +**/__tests__ +**/cypress + +**/logs +**/*.log + +**/tmp +**/temp + + diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index bc98890cb..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,84 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'eslint:recommended', - 'plugin:import/recommended', - 'plugin:import/typescript', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. - 'plugin:cypress/recommended', - ], - plugins: ['cypress'], - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.eslint.json'], - }, - settings: { - 'import/extensions': ['.js', '.ts'], - 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], - }, - 'import/resolver': { - typescript: { - project: 'packages/*/tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }], - '@typescript-eslint/explicit-member-accessibility': 'error', - 'no-console': 'error', - '@typescript-eslint/ban-ts-comment': 'warn', - '@typescript-eslint/consistent-type-imports': 'error', - 'import/no-cycle': 'error', - 'import/order': [ - 'error', - { - groups: ['type', ['builtin', 'external'], 'parent', 'sibling', 'index'], - alphabetize: { - order: 'asc', - }, - 'newlines-between': 'always', - }, - ], - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: false, - }, - ], - 'cypress/no-assigning-return-values': 'error', - 'cypress/no-unnecessary-waiting': 'error', - 'cypress/assertion-before-screenshot': 'warn', - 'cypress/no-force': 'warn', - 'cypress/no-async-tests': 'error', - 'cypress/no-pause': 'error', - }, - overrides: [ - { - files: ['jest.config.ts', '.eslintrc.js'], - env: { - node: true, - }, - }, - { - files: ['*.test.ts', '**/__tests__/**', '**/tests/**', 'jest.*.ts', 'samples/**'], - env: { - jest: true, - node: false, - }, - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - }, - ], - }, - }, - ], -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/actions/build_docker/action.yml b/.github/actions/build_docker/action.yml new file mode 100644 index 000000000..017b2a9b5 --- /dev/null +++ b/.github/actions/build_docker/action.yml @@ -0,0 +1,62 @@ +name: "Build Docker Image" +description: "Builds and pushes a Docker image to a registry" +inputs: + context: + description: "Docker build context" + required: true + dockerfile: + description: "Path to Dockerfile" + required: true + image_name: + description: "Name of the image" + required: true + registry: + description: "Container registry" + required: true + registry_username: + description: "Registry username" + required: true + registry_password: + description: "Registry password" + required: true + visibility: + description: "Package visibility (public or private)" + required: false + default: "private" +outputs: + image_tag: + description: "The tag of the built image" + value: ${{ steps.docker_build.outputs.image_tag }} +runs: + using: "composite" + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry_username }} + password: ${{ inputs.registry_password }} + + - name: Generate image tag + id: docker_build + shell: bash + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + TAG="pr-${{ github.event.number }}" + else + TAG="${{ github.sha }}" + fi + echo "image_tag=${TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + push: true + tags: ${{ inputs.registry }}/${{ inputs.image_name }}:${{ steps.docker_build.outputs.image_tag }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/actions/setup-indy-pool/action.yml b/.github/actions/setup-indy-pool/action.yml deleted file mode 100644 index cf9120e01..000000000 --- a/.github/actions/setup-indy-pool/action.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Setup Indy Pool -description: Setup an Indy ledger pool and register test did on the ledger -author: 'jan@animo.id' - -inputs: - seed: - description: Seed to register on the ledger - required: true - -runs: - using: composite - steps: - - name: Start indy pool - run: | - docker build -f network/indy-pool.dockerfile -t indy-pool . - docker run -d --name indy-pool -p 9701-9708:9701-9708 indy-pool - shell: bash - - - name: Setup Indy CLI - run: docker exec indy-pool indy-cli-setup - shell: bash - - - name: Register DID on ledger - run: docker exec indy-pool add-did-from-seed ${{ inputs.seed }} TRUSTEE - shell: bash - -branding: - icon: scissors - color: purple diff --git a/.github/actions/setup-libindy/action.yml b/.github/actions/setup-libindy/action.yml deleted file mode 100644 index cb83cf812..000000000 --- a/.github/actions/setup-libindy/action.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Setup Libindy -description: Download and install the libindy binary from the sovrin repository -author: 'jan@animo.id' - -runs: - using: composite - steps: - - name: Setup Indy - run: | - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 - sudo add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" - sudo apt-get update -y - sudo apt-get install -y --allow-unauthenticated libindy - shell: bash - -branding: - icon: scissors - color: purple diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml deleted file mode 100644 index 27fe8d108..000000000 --- a/.github/actions/setup-node/action.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Setup NodeJS -description: Setup NodeJS with caching -author: 'timo@animo.id' - -inputs: - node-version: - description: Node version to use - required: true - -runs: - using: composite - steps: - - name: Get yarn cache directory path - id: yarn-cache-dir-path - shell: bash - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@v2 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Setup node v${{ inputs.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ inputs.node-version }} - registry-url: 'https://registry.npmjs.org/' - -branding: - icon: scissors - color: purple diff --git a/.github/actions/test/action.yaml b/.github/actions/test/action.yaml new file mode 100644 index 000000000..39f6421c2 --- /dev/null +++ b/.github/actions/test/action.yaml @@ -0,0 +1,78 @@ +name: 'Run Tests' +description: 'Sets up Node.js environment and runs tests using Turbo' + +inputs: + node-version: + description: 'Node.js version to use' + required: false + default: '20' + pnpm-version: + description: 'PNPM version to use' + required: false + default: '8' + +outputs: + status: + description: 'Test execution status (success/failure)' + value: ${{ steps.test-status.outputs.status }} + +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ inputs.pnpm-version }} + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + shell: bash + run: pnpm install + + - name: Generate models + id: generate-models + shell: bash + run: pnpm run generate:models + + - name: Build libs + id: build-libs + shell: bash + run: pnpm run build:clean + + - name: Run tests + id: tests + shell: bash + run: pnpm run test + env: + OIDC_ROOT_ISSUER_URL: dummy + OIDC_ROOT_CLIENT_ID: dummy + OIDC_ROOT_CLIENT_SECRET: dummy + - name: Set test status + id: test-status + if: always() + shell: bash + run: | + if [ "${{ steps.tests.outcome }}" == "success" ]; then + echo "status=success" >> $GITHUB_OUTPUT + else + echo "status=failure" >> $GITHUB_OUTPUT + fi \ No newline at end of file diff --git a/.github/actions/trivy-scan/action.yaml b/.github/actions/trivy-scan/action.yaml new file mode 100644 index 000000000..096a3b0eb --- /dev/null +++ b/.github/actions/trivy-scan/action.yaml @@ -0,0 +1,37 @@ +name: "Trivy Image Scan" +description: "Scan Docker images using Trivy and output vulnerability summary" +inputs: + images: + description: "Comma-separated list of Docker images to scan" + required: true +runs: + using: "composite" + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Scan Docker Images + run: | + IFS=',' read -ra IMAGES <<< "${{ inputs.images }}" + for IMAGE in "${IMAGES[@]}"; do + echo "Scanning $IMAGE" + docker pull $IMAGE + trivy image --format json --output scan-results.json $IMAGE + + # Generate vulnerability summary + CRITICAL_COUNT=$(jq '.[].Vulnerabilities | map(select(.Severity == "CRITICAL")) | length' scan-results.json) + HIGH_COUNT=$(jq '.[].Vulnerabilities | map(select(.Severity == "HIGH")) | length' scan-results.json) + MEDIUM_COUNT=$(jq '.[].Vulnerabilities | map(select(.Severity == "MEDIUM")) | length' scan-results.json) + LOW_COUNT=$(jq '.[].Vulnerabilities | map(select(.Severity == "LOW")) | length' scan-results.json) + + echo "Vulnerability Summary for $IMAGE:" + echo "| Severity | Count |" + echo "|-----------|-------|" + echo "| Critical | $CRITICAL_COUNT |" + echo "| High | $HIGH_COUNT |" + echo "| Medium | $MEDIUM_COUNT |" + echo "| Low | $LOW_COUNT |" + echo "" + echo "---" + done + shell: bash \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index e21da151a..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,53 +0,0 @@ -# For details on how this file works refer to: -# - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file -version: 2 -updates: - # Maintain dependencies for GitHub Actions - # - Check for updates once a week - # - Group all updates into a single PR - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - groups: - all-actions: - patterns: [ "*" ] - - # Maintain dependencies for TypeScript and JavaScript - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "04:00" - timezone: "Canada/Pacific" - ignore: - - dependency-name: "*" - update-types: ["version-update:semver-major", "version-update:semver-patch"] - - # Maintain dependencies for docker - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "04:00" - timezone: "Canada/Pacific" - - # Maintain dependencies for docker - - package-ecosystem: "docker" - directory: "/client" - schedule: - interval: "weekly" - day: "monday" - time: "04:00" - timezone: "Canada/Pacific" - - # Maintain dependencies for docker - - package-ecosystem: "docker" - directory: "/server" - schedule: - interval: "weekly" - day: "monday" - time: "04:00" - timezone: "Canada/Pacific" \ No newline at end of file diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml deleted file mode 100644 index 78c74fbcc..000000000 --- a/.github/workflows/continuous-deployment.yml +++ /dev/null @@ -1,170 +0,0 @@ -name: Continuous Deployment - -on: - push: - branches: ['release'] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - CLIENT: client - SERVER: server - -jobs: - cypress-run: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Setup NodeJS - uses: ./.github/actions/setup-node - with: - node-version: 16 - - - name: Start App - run: | - yarn - NODE_ENV=test AGENT_WALLET_KEY=ba8e2462-f9ac-4da0-a9c6-99592a7dbc12 REACT_APP_HOST_BACKEND=http://localhost:5000 AGENT_PUBLIC_DID_SEED=testtesttesttesttesttesttesttest yarn dev & - - - name: Wait for credential defs to be created - run: sleep 120s - shell: bash - - - name: Cypress run - uses: cypress-io/github-action@v2 - with: - record: true - browser: chrome - env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - build-and-push-image-server: - needs: - - cypress-run - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - defaults: - run: - working-directory: ${{ env.SERVER }} - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Setup NodeJS - uses: ./.github/actions/setup-node - with: - node-version: 16 - - - name: Install dependencies - run: yarn install - - - run: yarn build - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.SERVER }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: ./server - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - build-and-push-image-client: - needs: - - cypress-run - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - defaults: - run: - working-directory: ${{ env.CLIENT }} - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Install dependencies - run: yarn install - - - run: REACT_APP_INSIGHTS_PROJECT_ID=${{ secrets.REACT_APP_INSIGHTS_PROJECT_ID }} REACT_APP_HOST_BACKEND=${{ secrets.REACT_APP_HOST_BACKEND }} yarn build - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.CLIENT }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: ./client - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - deploy: - runs-on: ubuntu-latest - needs: - - build-and-push-image-client - - build-and-push-image-server - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Copy stack file to remote - uses: garygrossgarten/github-action-scp@v0.7.3 - with: - local: docker-compose.yml - remote: animo-demo/docker-compose.yml - host: dashboard.dev.animo.id - username: root - privateKey: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} - - - name: Deploy to Docker Swarm via SSH action - uses: appleboy/ssh-action@v0.1.4 - env: - AGENT_PUBLIC_DID_SEED: ${{ secrets.AGENT_PUBLIC_DID_SEED }} - AGENT_WALLET_KEY: ${{ secrets.AGENT_WALLET_KEY }} - with: - host: dashboard.dev.animo.id - username: root - key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} - envs: AGENT_PUBLIC_DID_SEED,AGENT_WALLET_KEY - script: | - AGENT_PUBLIC_DID_SEED=${AGENT_PUBLIC_DID_SEED} AGENT_WALLET_KEY=${AGENT_WALLET_KEY} docker stack deploy --compose-file animo-demo/docker-compose.yml demo --with-registry-auth diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml deleted file mode 100644 index d453afa44..000000000 --- a/.github/workflows/continuous-integration.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Continuous Integration - -on: - pull_request: - branches: [main, release] - -jobs: - cypress-run: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Setup NodeJS - uses: ./.github/actions/setup-node - with: - node-version: 16 - - - name: Install dependencies - run: yarn install - - - name: Linting - run: yarn lint - - - name: Prettier - run: yarn format - - - name: Compile - run: yarn check-types - - - name: Start App - run: | - yarn - NODE_ENV=test AGENT_WALLET_KEY=ba8e2462-f9ac-4da0-a9c6-99592a7dbc12 REACT_APP_HOST_BACKEND=http://localhost:5000 AGENT_PUBLIC_DID_SEED=testtesttesttesttesttesttesttest yarn dev & - - - name: Wait for credential defs to be created - run: sleep 120s - shell: bash - - - name: Cypress run - uses: cypress-io/github-action@v2 - with: - record: true - browser: chrome - env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml deleted file mode 100644 index c09165ff2..000000000 --- a/.github/workflows/deployment.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: Deployment - -on: - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - CLIENT: client - SERVER: server - -jobs: - build-and-push-image-server: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - defaults: - run: - working-directory: ${{ env.SERVER }} - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Setup NodeJS - uses: ./.github/actions/setup-node - with: - node-version: 16 - - - name: Install dependencies - run: yarn install - - - run: yarn build - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.SERVER }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: ./server - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - build-and-push-image-client: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - defaults: - run: - working-directory: ${{ env.CLIENT }} - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Setup Libindy - uses: ./.github/actions/setup-libindy - - - name: Install dependencies - run: yarn install - - - run: REACT_APP_INSIGHTS_PROJECT_ID=${{ secrets.REACT_APP_INSIGHTS_PROJECT_ID }} REACT_APP_HOST_BACKEND=${{ secrets.REACT_APP_HOST_BACKEND }} yarn build - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.CLIENT }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: ./client - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - deploy: - runs-on: ubuntu-latest - needs: - - build-and-push-image-client - - build-and-push-image-server - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Copy stack file to remote - uses: garygrossgarten/github-action-scp@v0.7.3 - with: - local: docker-compose.yml - remote: animo-demo/docker-compose.yml - host: dashboard.dev.animo.id - username: root - privateKey: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} - - - name: Deploy to Docker Swarm via SSH action - uses: appleboy/ssh-action@v0.1.4 - env: - AGENT_PUBLIC_DID_SEED: ${{ secrets.AGENT_PUBLIC_DID_SEED }} - AGENT_WALLET_KEY: ${{ secrets.AGENT_WALLET_KEY }} - with: - host: dashboard.dev.animo.id - username: root - key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} - envs: AGENT_PUBLIC_DID_SEED,AGENT_WALLET_KEY - script: | - AGENT_PUBLIC_DID_SEED=${AGENT_PUBLIC_DID_SEED} AGENT_WALLET_KEY=${AGENT_WALLET_KEY} docker stack deploy --compose-file animo-demo/docker-compose.yml demo --with-registry-auth diff --git a/.github/workflows/on_pr_closed.yaml b/.github/workflows/on_pr_closed.yaml new file mode 100644 index 000000000..552c38cc1 --- /dev/null +++ b/.github/workflows/on_pr_closed.yaml @@ -0,0 +1,109 @@ +name: Uninstall PR resources +on: + # Add manual trigger with PR number input + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to uninstall' + required: true + type: string + pull_request: + branches: + - main + - showcase-builder + paths: + - "apps/**" + - "charts/**" + types: + - closed + +jobs: + + uninstall: + name: Uninstall PR Resources + environment: development + runs-on: ubuntu-latest + if: github.repository_owner == 'bcgov' + steps: + # Set PR number based on event type + - name: Set PR number + id: pr_number + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "PR_NUM=${{ github.event.inputs.pr_number }}" >> $GITHUB_ENV + else + echo "PR_NUM=${{ github.event.number }}" >> $GITHUB_ENV + fi + + - name: Install OpenShift CLI tools + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.14" + + - name: Authenticate and set context + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} + certificate_authority_data: ${{ secrets.OPENSHIFT_CA_CRT }} + namespace: ${{ secrets.OPENSHIFT_DEV_NAMESPACE }} + + - name: Uninstall Credential Showcase resources via Helm + continue-on-error: true + run: | + helm uninstall pr-${{ env.PR_NUM }}-bc-wallet --namespace ${{ secrets.OPENSHIFT_DEV_NAMESPACE }} + + - name: Remove Credential Showcase Openshift Objects + continue-on-error: true + run: | + oc delete secret,pvc,route,service,deployment,statefulset,configmap \ + --selector "app.kubernetes.io/instance=pr-${{ env.PR_NUM }}-bc-wallet" + + clean-ghcr: + runs-on: ubuntu-latest + name: Delete closed PR images + steps: + - name: Delete API Server Image + uses: snok/container-retention-policy@v3.0.0 + with: + account: ${{ github.repository_owner }} + image-names: bc-wallet-api-server + image-tags: ${{ github.event.number != '' && format('pr-{0}', github.event.number) || format('pr-{0}', github.event.inputs.pr_number) }} + cut-off: 1second + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Delete Traction Adapter Image + uses: snok/container-retention-policy@v3.0.0 + with: + account: ${{ github.repository_owner }} + image-names: bc-wallet-traction-adapter + image-tags: ${{ github.event.number != '' && format('pr-{0}', github.event.number) || format('pr-{0}', github.event.inputs.pr_number) }} + cut-off: 1second + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Delete Showcase Creator Image + uses: snok/container-retention-policy@v3.0.0 + with: + account: ${{ github.repository_owner }} + image-names: bc-wallet-showcase-creator + image-tags: ${{ github.event.number != '' && format('pr-{0}', github.event.number) || format('pr-{0}', github.event.inputs.pr_number) }} + cut-off: 1second + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Delete Demo Server Image + uses: snok/container-retention-policy@v3.0.0 + with: + account: ${{ github.repository_owner }} + image-names: bc-wallet-demo-server + image-tags: ${{ github.event.number != '' && format('pr-{0}', github.event.number) || format('pr-{0}', github.event.inputs.pr_number) }} + cut-off: 1second + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Delete Demo Web Image + uses: snok/container-retention-policy@v3.0.0 + with: + account: ${{ github.repository_owner }} + image-names: bc-wallet-demo-web + image-tags: ${{ github.event.number != '' && format('pr-{0}', github.event.number) || format('pr-{0}', github.event.inputs.pr_number) }} + cut-off: 1second + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/on_pr_opened.yaml b/.github/workflows/on_pr_opened.yaml index f80ada055..9a8d06b65 100644 --- a/.github/workflows/on_pr_opened.yaml +++ b/.github/workflows/on_pr_opened.yaml @@ -3,7 +3,7 @@ name: PR Build and Deploy on: pull_request: branches: - # - main + # - main - showcase-builder paths: - "apps/**" @@ -67,7 +67,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} - visibility: "public" + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-api-server:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -91,7 +95,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} - visibility: "public" + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-traction-adapter:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -115,7 +123,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} - visibility: "public" + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-showcase-creator:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -139,7 +151,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} - visibility: "public" + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-demo-server:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -163,26 +179,91 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} - visibility: "public" + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-demo-web:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} - image-scan: - name: "Image Scan" - needs: [build_api_server, build_traction_adapter, build_showcase_creator, build_demo_server, build_demo_web, ready] + deploy: + name: "Deploy PR Environment" + environment: development runs-on: ubuntu-latest - timeout-minutes: 10 + needs: + - ready + - build_api_server + - build_traction_adapter + - build_showcase_creator + - build_demo_server + - build_demo_web + if: ${{ always() && fromJSON(needs.ready.outputs.deploy) == true && !(contains(needs.*.result, 'failure')) }} + steps: + - uses: actions/checkout@v4 + + - name: Install OpenShift CLI tools + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.14" + + - name: Authenticate and set context + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} + certificate_authority_data: ${{ secrets.OPENSHIFT_CA_CRT }} + namespace: ${{ secrets.OPENSHIFT_NAMESPACE }} + + - name: Deploy PR Environment with Helm + run: | + cd charts/bc-wallet + helm upgrade --install pr-${{ github.event.number }}-bc-wallet --namespace ${{ vars.OPENSHIFT_NAMESPACE }} \ + --set global.extraEnvVarsSecret=bc-wallet-secrets \ + --set apiServer.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ + --set tractionAdapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ + --set showcaseCreator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ + --set demoWeb.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ + --set demoServer.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ + . --wait + + - name: Restart PR Deployments + run: | + oc rollout restart deployment/pr-${{ github.event.number }}-bc-wallet-api-server + oc rollout restart deployment/pr-${{ github.event.number }}-bc-wallet-traction-adapter + oc rollout restart deployment/pr-${{ github.event.number }}-bc-wallet-showcase-creator + oc rollout restart deployment/pr-${{ github.event.number }}-bc-wallet-demo-server + oc rollout restart deployment/pr-${{ github.event.number }}-bc-wallet-demo-web + + deploymenturls: + name: PR Deployment URLs comment + runs-on: ubuntu-latest + needs: + - deploy if: ${{ always() && fromJSON(needs.ready.outputs.deploy) == true && !(contains(needs.*.result, 'failure')) }} steps: - - name: Run Trivy Image Scan - uses: ./.github/actions/trivy-scan + - uses: actions/checkout@v4 + + - name: Find Comment + uses: peter-evans/find-comment@v3 + id: fc with: - images: | - ghcr.io/${{ github.repository_owner }}/bc-wallet-api-server:${{ needs.build_api_server.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-traction-adapter:${{ needs.build_traction_adapter.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-demo-web:${{ needs.build_demo_web.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-showcase-creator:${{ needs.build_showcase_creator.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-demo-server:${{ needs.build_demo_server.outputs.image_tag }} -# scan-ref: '' -# scan-title: '' -# scan-type: '' + issue-number: ${{ github.event.number }} + comment-author: "github-actions[bot]" + body-includes: PR Deployment URLs ready for review. + + - name: Create comment + if: steps.fc.outputs.comment-id == '' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.number }} + body: | + | Deployment | Resource | Location | + | --- | --- | --- | + | API Server | | https://pr-${{ github.event.number }}-bc-wallet-api-server-dev.apps.silver.devops.gov.bc.ca/ | + | Traction Adapter | | https://pr-${{ github.event.number }}-bc-wallet-traction-adapter-dev.apps.silver.devops.gov.bc.ca/ | + | Demo Web | | https://pr-${{ github.event.number }}-bc-wallet-demo-web-dev.apps.silver.devops.gov.bc.ca/ | + | Showcase Creator | | https://pr-${{ github.event.number }}-bc-wallet-showcase-creator-dev.apps.silver.devops.gov.bc.ca/ | + | Demo Server | | https://pr-${{ github.event.number }}-bc-wallet-demo-server-dev.apps.silver.devops.gov.bc.ca/ | + + PR Deployment URLs ready for review. diff --git a/.github/workflows/on_push_main.yaml b/.github/workflows/on_push_main.yaml index ef83ea779..7cbc3f00c 100644 --- a/.github/workflows/on_push_main.yaml +++ b/.github/workflows/on_push_main.yaml @@ -1,4 +1,4 @@ -name: Build, Push and Deploy +name: Build, Push and Deploy on: workflow_dispatch: push: @@ -6,13 +6,17 @@ on: # - main - showcase-builder paths: - - "apps/bc-wallet-api-server/**" - - "apps/bc-wallet-traction-adapter/**" - - "apps/bc-wallet-showcase-creator/**" - - "apps/bc-wallet-demo-server/**" - - "apps/bc-wallet-demo-web/**" - - "charts/**" - - ".github/**" + - "apps/bc-wallet-api-server/**" + - "apps/bc-wallet-traction-adapter/**" + - "apps/bc-wallet-showcase-creator/**" + - "apps/bc-wallet-demo-server/**" + - "apps/bc-wallet-demo-web/**" + - "charts/**" + - ".github/**" +permissions: + contents: read + packages: write + security-events: write jobs: build_api_server: @@ -31,6 +35,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-api-server:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -50,6 +59,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-traction-adapter:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -69,6 +83,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-showcase-creator:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -88,6 +107,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-demo-server:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -107,6 +131,11 @@ jobs: registry: ghcr.io registry_username: ${{ github.repository_owner}} registry_password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/${{ github.repository_owner}}/bc-wallet-demo-web:${{ steps.builder.outputs.image_tag }} + format: table outputs: image_tag: ${{ steps.builder.outputs.image_tag }} @@ -135,18 +164,18 @@ jobs: openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} certificate_authority_data: ${{ secrets.OPENSHIFT_CA_CRT }} - namespace: ${{ secrets.OPENSHIFT_NAMESPACE }} + namespace: ${{ secrets.OPENSHIFT_DEV_NAMESPACE }} - name: Deploy to OpenShift with Helm run: | cd charts/bc-wallet - helm dependency update - helm upgrade --install bc-wallet-demo --namespace ${{ secrets.OPENSHIFT_NAMESPACE }} -f ./values.yaml \ - --set api_server.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ - --set traction_adapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ - --set showcase_creator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ - --set demo_web.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ - --set demo_server.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ + helm upgrade --install bc-wallet-demo --namespace ${{ vars.OPENSHIFT_NAMESPACE }} \ + --set global.extraEnvVarsSecret=bc-wallet-secrets \ + --set apiServer.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ + --set tractionAdapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ + --set showcaseCreator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ + --set demoWeb.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ + --set demoServer.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ . --wait - name: Restart Deployments @@ -181,13 +210,12 @@ jobs: - name: Deploy to OpenShift with Helm run: | cd charts/bc-wallet - helm dependency update - helm upgrade --install bc-wallet-demo --namespace ${{ secrets.OPENSHIFT_NAMESPACE }} -f ./values.yaml \ - --set api_server.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ - --set traction_adapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ - --set showcase_creator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ - --set demo_web.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ - --set demo_server.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ + helm upgrade --install bc-wallet-demo --namespace ${{ vars.OPENSHIFT_NAMESPACE }} -f ./values.yaml \ + --set apiServer.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ + --set tractionAdapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ + --set showcaseCreator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ + --set demoWeb.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ + --set demoServer.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ . --wait - name: Restart Deployments @@ -222,13 +250,12 @@ jobs: - name: Deploy to OpenShift with Helm run: | cd charts/bc-wallet - helm dependency update - helm upgrade --install bc-wallet-demo --namespace ${{ secrets.OPENSHIFT_NAMESPACE }} -f ./values.yaml \ - --set api_server.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ - --set traction_adapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ - --set showcase_creator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ - --set demo_web.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ - --set demo_server.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ + helm upgrade --install bc-wallet-demo --namespace ${{ vars.OPENSHIFT_NAMESPACE }} -f ./values.yaml \ + --set apiServer.image.tag=${{ needs.build_api_server.outputs.image_tag }} \ + --set tractionAdapter.image.tag=${{ needs.build_traction_adapter.outputs.image_tag }} \ + --set showcaseCreator.image.tag=${{ needs.build_showcase_creator.outputs.image_tag }} \ + --set demoWeb.image.tag=${{ needs.build_demo_web.outputs.image_tag }} \ + --set demoServer.image.tag=${{ needs.build_demo_server.outputs.image_tag }} \ . --wait - name: Restart Deployments @@ -238,23 +265,3 @@ jobs: oc rollout restart deployment/bc-wallet-demo-showcase-creator oc rollout restart deployment/bc-wallet-demo-demo-server oc rollout restart deployment/bc-wallet-demo-demo-web - - image-scan: - name: "Image Scan" - needs: - - build_api_server - - build_traction_adapter - - build_showcase_creator - - build_demo_server - - build_demo_web - runs-on: ubuntu-latest - steps: - - name: Run Trivy Image Scan - uses: ./.github/actions/trivy-scan - with: - images: | - ghcr.io/${{ github.repository_owner }}/bc-wallet-api-server:${{ needs.build_api_server.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-traction-adapter:${{ needs.build_traction_adapter.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-demo-web:${{ needs.build_demo_web.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-showcase-creator:${{ needs.build_showcase_creator.outputs.image_tag }}, - ghcr.io/${{ github.repository_owner }}/bc-wallet-demo-server:${{ needs.build_demo_server.outputs.image_tag }} diff --git a/.gitignore b/.gitignore index 179146e95..0e7a4f2a0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* -# Diagnostic reports (https://nodejs.org/api/report.html) + +# Diagnostic reports report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data @@ -14,42 +15,40 @@ pids *.seed *.pid.lock -# Directory for instrumented libs generated by jscoverage/JSCover +# Directory for instrumented libs +typings/ lib-cov +lib/*.js +test/*.js +*.map -# Coverage directory used by tools like istanbul +# Coverage directories coverage +coverage.* *.lcov - -# nyc test coverage .nyc_output -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +# Grunt intermediate storage .grunt -# Bower dependency directory (https://bower.io/) +# Bower dependency directory bower_components # node-waf configuration .lock-wscript -# Compiled binary addons (https://nodejs.org/api/addons.html) +# Compiled binary addons build/Release # Dependency directories node_modules/ jspm_packages/ -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - # Optional npm cache directory .npm +.npmrc -# Optional eslint cache +# ESLint cache .eslintcache # Microbundle cache @@ -62,30 +61,29 @@ typings/ .node_repl_history # Output of 'npm pack' -*.tgz +apps/**/*.tgz +packages/**/*.tgz # Yarn Integrity file .yarn-integrity -# dotenv environment variables file -.env -.env.test +# dotenv files +**/.env +**/.env.* +!**/.env.example -# parcel-bundler cache (https://parceljs.org/) +# Parcel-bundler cache .cache # Next.js build output .next -# Nuxt.js build / generate output +# Nuxt.js build output .nuxt dist # Gatsby files .cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public # vuepress build output .vuepress/dist @@ -101,8 +99,33 @@ dist # TernJS port file .tern-port -client/src/.env.local -server/logs.txt + +# Apps environment and logs +apps/*/src/.env.local +apps/*/logs.txt .DS_Store -build \ No newline at end of file +.DS_STORE +build + +.idea +.vscode +*.swp +*~ + +.turbo + +# Ignore Open API generated packages +packages/**/dist +packages/**/target +/packages/*/dist +/apps/*/dist +**/bc-wallet-openapi/src/**/* +**/bc-wallet-traction-openapi/src/**/* + +**/.env +**/.env.* +!**/.env.example + + +**/*.tsbuildinfo diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..cb91b9020 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# Include patterns from .gitignore +!node_modules +!.git + +# Specific patterns to ignore +apps/*/src/showcase-api/** +**/build/** +**/.next/** +**/charts/** \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index cbe842acd..3fbcb20e2 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "printWidth": 120, "semi": false, - "singleQuote": true + "singleQuote": true, + "endOfLine": "lf" } diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f0b7fca00..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cSpell.words": ["bcgov-black", "bcgov-darkgrey", "bcgov-lightgrey"] -} diff --git a/README.md b/README.md index ccc6651d3..bd7c2bb5e 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,228 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Lifecycle:Maturing](https://img.shields.io/badge/Lifecycle-Maturing-007EC6)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) -# BC Wallet Demo +# BC Wallet Showcase -## Overview +**Version:** 0.1.0 +**License:** Apache-2.0 -This application provides a showcase for the BC Wallet to illustrate the use cases for verifiable credentials. This application will take users through multiple steps to demonstrate how verifiable credentials are issued and verified using the BC Wallet. +## 📝 Module Overview + +The **Wallet Showcase** project bring an **Interactive Digital Credential Showcase Builder** allowing less technically inclined people to create showcases and demo scenarios with +verifiable credentials. The showcases then are automatically made available on a **Demo wallet** that can be used to run the scenario's. It serves as a tool to quickly create +demo's showing the power of verifiable credential technology in different use cases. + +### 💡 Purpose + +- Easily create demo's for different use cases of Verifiable Credentials. +- Allow experimentation and rapid setup for people new to Verifiable Credential technology. + +### ⚙️ Core Functionalities + +- **Showcase Builder:** An Interactive User Interface to build showcases containing personas, scenarios and steps, as well as creating credential definitions, without the need of + much technical knowledge. +- **Demo Wallet:** The Demo wallet acts as the Issuer and Relying party frontend, showing the showcases, personas, scenarios and steps. This is what an end-user will see after + showcases have been created by the Showcase builder. +- **Showcase API** The Showcase API is a REST API that serves as the technology neutral data storage between different components, of which the wallet and builder are the most + noticeable components. The API is credential technology neutral. It has no direct integrations with any credential platform/library. +- **Traction Adapter** This is an adapter that translates the neutral API into AcaPY/Traction specific credential actions. It serves is the first of potentially many different + integrations points with future credential technologies. + +### 🏆 Key Benefits +- **Easy of use:** The showcase builder is designed to be used by less technically inclined users, allowing them to experiment with different showcases +- **Rapid prototyping:** The showcase builder allows you to rapidly create an entire new showcase, or add new scenarios to existing showcases. This allows for rapid experimentation and customization +- **Multi-tenant:** The showcase builder, API and wallet are designed as multi-tenant solutions, allowing to make the tool available to different departments, or even external customers +- **Flexibility:** Adapters can be swapped or extended to support different credential formats (e.g., SD-JWT, OID4VCI) and wallets in the future. +- **Resilience:** Durable messaging to handle temporary outages without data loss. + +## 📁 Project Structure + +``` +apps/ +├── bc-wallet-api-server/ # This is the Showcase REST API providing the credential agnostic data model +├── bc-wallet-demo-server/ # The server/backend of the web wallet delegating to AcaPy and Traction +├── bc-wallet-demo-web/ # The frontend of the web wallet showing the actual showcases, personas and scenarios +├── bc-wallet-showcase-creator/ # The frontend to design and manage showcases, scenarios, credential definitions and personas +charts/ # Helm charts for deployment to Kubernetes +packages/ +├── bc-wallet-openapi/ # The Showcase OpenAPI definition for the REST API and models +├── bc-wallet-traction-adapter # The AcaPY/Traction integration (asynchronous using a message broker) +resources/ # Documentation resources +├── DEVELOPER # Developer docs +├── DEVOPS/ # DevOps docs +└── README.md # Project documentation (this file) +``` +## 📦 Package Management + +This project uses **pnpm** and **turbo** for monorepo and package management. ## Running + ### Copy env files -Copy the files server/.env.example and client/.env.example to server/.env and client/.env -Edit the .env files to match your project needs - - -### Option 1 - Native -Please make sure you have a recent version of node, npm, and yarn installed -These steps are executed from the root folder of the project: - -> yarn install - -> yarn dev - -The application will now be running at http://localhost:3000 - -### Option 2 - Docker -These steps assume that you have docker installed - -These steps are executed from the root folder of the project: - -Build the client: -> docker build -t bc-wallet-demo-client . -f DockerfileClient - -Build the server: -> docker build -t bc-wallet-demo-server . -f DockerfileServer - -Start the server: -> docker run --name bc-wallet-demo-server -p5000:5000 --rm --env-file server/.env bc-wallet-demo-server - -Start the client: -> docker run --name bc-wallet-demo-client -p3000:3000 -v \`pwd\`/Caddyfile:/etc/caddy/Caddyfile --rm --env-file client/.env bc-wallet-demo-client - -The application will now be running at http://localhost:3000 +For each of the apps under the apps/ directory, you'll need to create a ```.env``` file from the corresponding ```.env.example``` template. This ensures all environment variables are set up correctly before running the applications. + +```bash +cp apps/bc-wallet-api-server/.env.example apps/bc-wallet-api-server/.env +cp apps/bc-wallet-demo-server/.env.example apps/bc-wallet-demo-server/.env +cp apps/bc-wallet-demo-web/.env.example apps/bc-wallet-demo-web/.env +cp apps/bc-wallet-showcase-creator/.env.example apps/bc-wallet-showcase-creator/.env +cp apps/bc-wallet-traction-adapter/.env.example apps/bc-wallet-traction-adapter/.env +``` + + +Additionally, for the Docker development environment, copy the Docker env file: + +```bash +cp docker/dev/.env.example docker/dev/.env +``` + +### Environment Configuration – API URL Setup + +To ensure the frontend correctly communicates with the backend API, update your `.env` variables based on your environment: + +```bash +# For local development without Docker +NEXT_PUBLIC_SHOWCASE_API_URL=http://localhost:5005 + +# For Docker Compose setup (inside containers) +# Use host.docker.internal to allow Docker containers to reach services on the host machine +NEXT_PUBLIC_SHOWCASE_API_URL=http://host.docker.internal:5005 +``` + +> host.docker.internal allows Docker containers to access services running on your host machine, solving the issue where localhost inside a container points to the container itself instead of the host. + +>📌 Important: After copying, review each .env file (including docker/dev/.env) and update the values as needed to match your local setup or deployment environment. + +### 🐳 Option 1 - Docker + +>These steps assume that Docker and Docker Compose are installed on your machine. + +>⚠️ Make sure your .env files are properly copied from docker/dev/.env.example + + + +#### You can run all apps using Docker. + + +```bash +docker compose up -d +``` +This will start all services + +To stop and remove all running containers and associated volumes, run: + +```bash +docker compose down -v +``` + +### Option 2 - Native + +### Make sure pnpm is installed + +```shell +npm -g install pnpm +``` + +### Install Dependencies + +```bash +pnpm install +``` + +#### Generate Modals + +```bash +pnpm generate:models +``` + +## Start Required Services with Docker + +>These steps assume that Docker and Docker Compose are installed on your machine. + +Before running the applications, ensure that PostgreSQL and RabbitMQ are up and running. You can start each service separately using Docker: + +### Start PostgreSQL + +```bash +docker compose up -d postgres +``` + +### Start RabbitMQ + +```bash +docker compose up -d rabbitmq +``` + +### Start the Project + +```bash +pnpm dev +``` + +#### Run Tests + +```bash +pnpm test +``` + + +#### Create a Tenant for Showcase Builder + +After starting all services and the project is running, you'll need to create a tenant in order to use the Showcase Builder. + +📘 For detailed instructions, please refer to the [README](./apps/bc-wallet-api-server/README.md) file + + +#### 🚀 Local Development URLs + +After starting the apps, you can access them at the following local addresses: + +#### Showcase Creator Access + +Access the Showcase Creator using the following URL format: + +```bash +http://localhost:5003/en//login +``` + +For example, if your tenant ID is showcase-tenantA, the URL would be: + +```bash +http://localhost:5003/en/showcase-tenantA/login +``` + +- API Server: http://localhost:5005 + +- Demo Web: http://localhost:5002/digital-trust/showcase/ + + +### Build the Project + +```bash +pnpm build +``` +> 🔄 After building, it's recommended to clean up any build artifacts to avoid workspace conflicts in development. + +### clean build artifacts +Use this command to remove any generated build files (like .next folders) + +```bash +pnpm clean +``` + +For CI/CD pipelines: + +```bash +pnpm test:ci +``` + + +--- + +## 🏷️ License + +This project is licensed under the **Apache-2.0** license. + ## Contributing @@ -48,8 +230,8 @@ The application will now be running at http://localhost:3000 Please see the [Contributions Guide](CONTRIBUTING.md) for the repo. -Before contributing please run `yarn lint --fix` and fix any linter warnings in your code contribution. +Before contributing please run `pnpm lint --fix` and fix any linter warnings in your code contribution. You may also create an issue if you would like to suggest additional resources to include in this repository. -All contrbutions to this repository should adhere to our [Code of Conduct](./CODE_OF_CONDUCT). \ No newline at end of file +All contributions to this repository should adhere to our [Code of Conduct](./CODE_OF_CONDUCT). \ No newline at end of file diff --git a/apps/bc-wallet-api-server/.dockerignore b/apps/bc-wallet-api-server/.dockerignore new file mode 100644 index 000000000..8e36a023e --- /dev/null +++ b/apps/bc-wallet-api-server/.dockerignore @@ -0,0 +1,23 @@ +**/node_modules +**/dist +**/build +**/coverage + +.git +.github +.gitignore + +**/.vscode +**/.idea +**/.DS_Store + +**/test +**/tests +**/__tests__ +**/cypress + +**/logs +**/*.log + +**/tmp +**/temp \ No newline at end of file diff --git a/apps/bc-wallet-api-server/.env.example b/apps/bc-wallet-api-server/.env.example new file mode 100644 index 000000000..272f0d632 --- /dev/null +++ b/apps/bc-wallet-api-server/.env.example @@ -0,0 +1,31 @@ +# application settings +PORT=5005 + +# The database connection can be established using a database url or using separate params +SHOWCASE_DB_NAME=postgres +SHOWCASE_DB_HOST=localhost +SHOWCASE_DB_PORT=5432 +# SHOWCASE_DB_PORT=5232 # for local development +SHOWCASE_DB_USERNAME=postgres +SHOWCASE_DB_PASSWORD=your-super-secret-and-long-postgres-password + +# cors settings +CORS_DISABLED=false +CORS_ALLOW_ORIGINS=http://localhost:5003,http://localhost:5002 +CORS_ALLOW_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOW_HEADERS=Content-Type,Authorization,X-Requested-With,Cache-Control,Pragma,Expires +CORS_ALLOW_CREDENTIALS=true + +# authentication & authorization +REALM=YOUR_REALM +OIDC_ROOT_ISSUER_URL=YOUR_AUTH_SERVER_BASE_URL +OIDC_ROOT_CLIENT_ID=showcase-root +OIDC_ROOT_CLIENT_SECRET=YOUR_CLIENT_SECRET +MULTITENANCY_MODE=multitenant + +# Traction message broker settings +AMQ_HOST=localhost +AMQ_PORT=5672 +AMQ_TRANSPORT=tcp # tls for non-local instances +ENCRYPTION_KEY=TS9PmByXiBMmAUFSNubUzMiWWJmMCjWa2DGwgFnCaas # for production instances create a new one using the generate-key script. The key should match between bc-wallet-traction-adapter and bc-wallet-api-server +NONCE_SIZE=12 \ No newline at end of file diff --git a/apps/bc-wallet-api-server/Dockerfile b/apps/bc-wallet-api-server/Dockerfile new file mode 100644 index 000000000..fc38c09fc --- /dev/null +++ b/apps/bc-wallet-api-server/Dockerfile @@ -0,0 +1,50 @@ +# Build stage +FROM node:20-bookworm AS builder +RUN apt update && apt install -y maven openjdk-17-jdk + +SHELL ["/bin/bash", "-c"] +ENV SHELL=bash +ENV PNPM_HOME=/usr/local/share/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN npm -g install pnpm && pnpm setup && source /root/.bashrc && pnpm self-update && pnpm setup && pnpm config set store-dir "/usr/local/share/pnpm/store/v10" --global + +# Install Maven and other dependencies +RUN apt-get update && \ + apt-get install -y maven build-essential python3 make g++ openjdk-17-jdk && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /build +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.json tsconfig.build.json tsconfig.eslint.json ./ +COPY packages packages +COPY apps/tsconfig-base.json apps/ +COPY apps/bc-wallet-api-server apps/bc-wallet-api-server + +RUN rm -rf $(pnpm store path) && pnpm install -g rimraf typescript tslib drizzle-kit +RUN pnpm install +RUN pnpm generate:models +RUN pnpm build:clean # FIXME turbo does not build this correctly in docker compose for unknown reasons, so do a pre-build +RUN pnpm build +RUN pnpm deploy /deploy --filter bc-wallet-api-server --prod --no-optional + +# Runtime stage +FROM node:20-bookworm-slim + +ARG uid=1001 +ARG user=nodeapp +RUN useradd -U -ms /bin/bash -u $uid $user && usermod -a -G 0 $user +WORKDIR /app +COPY --from=builder /deploy /app +COPY --from=builder /build/apps/bc-wallet-api-server/src/database/migrations /app/dist/database/migrations + +ENV NODE_ENV=production +RUN echo PORT=3000 > .env + +RUN echo '#!/bin/sh\necho "Connecting with database to start..."\nsleep 10\necho "Starting application..."\nexec node dist/index.js' > /start.sh && \ + chmod +x /start.sh + +RUN chown -R $user:root /app && chmod -R ug+rw /app + +USER $user +EXPOSE 3000 + +CMD ["/start.sh"] diff --git a/apps/bc-wallet-api-server/LICENSE b/apps/bc-wallet-api-server/LICENSE new file mode 100644 index 000000000..c2ae89725 --- /dev/null +++ b/apps/bc-wallet-api-server/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2025] [4Sure] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/bc-wallet-api-server/README.md b/apps/bc-wallet-api-server/README.md new file mode 100644 index 000000000..7bef49571 --- /dev/null +++ b/apps/bc-wallet-api-server/README.md @@ -0,0 +1,227 @@ +# bc-wallet-api-server + +## Environment variables + +Please see [.env.example](.env.example) for a list and explanation of all the environment variables. + +## Database + +There are several environment variables for connecting to a postgres database. A connection url can be used or individual options can be set to connect. +If a connection url is set than this will be favored. + +> **_NOTE:_** Currently only PostgreSQL support is available + +### Migrations + +We generate database migrations using [Drizzle kit](https://orm.drizzle.team/docs/kit-overview), which manages and versions database schema changes. These can be found at `./src/database/migrations`. + +#### Generate migrations + +Migrations can be generated using the following command + +```shell +pnpm migration:generate +``` + +## Installation + +```shell +pnpm install bc-wallet-api-server +``` + +## Build + +```shell +pnpm build +``` + + +## Tenant Management + +### Authentication + +Before performing any tenant operations, you'll need to authenticate and obtain an access token. + +#### PowerShell +```powershell +$tokenResponse = Invoke-RestMethod -Method POST ` + -Uri "https://auth-server/auth/realms/BC/protocol/openid-connect/token" ` + -ContentType "application/x-www-form-urlencoded" ` + -Body "grant_type=client_credentials&client_id=showcase-root&client_secret=your_keycloak_root_client_secret" + +$token = $tokenResponse.access_token +``` + +#### cURL +```bash +token=$(curl -s -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=showcase-root&client_secret=your_keycloak_root_client_secret" \ + https://auth-server/auth/realms/BC/protocol/openid-connect/token | jq -r '.access_token') +``` + +### Create a Tenant + +#### PowerShell +```powershell +$tenantData = @{ + id = "showcase-tenantA" + oidcIssuer = "https://auth-server/auth/realms/BC" + tractionTenantId = "your_traction_tenant_id" + tractionWalletId = "your_traction_wallet_id" + tractionApiUrl = "https://traction-tenant-proxy-test.apps.silver.devops.gov.bc.ca" + tractionApiKey = "your_traction_api_key" +} + +$jsonBody = $tenantData | ConvertTo-Json + +$response = Invoke-RestMethod ` + -Uri "http://localhost:5005/tenants" ` + -Method Post ` + -Headers @{ + "Authorization" = "Bearer $token" + "Content-Type" = "application/json" + } ` + -Body $jsonBody +``` + +#### cURL +```bash +curl -X POST \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "showcase-tenantA", + "oidcIssuer": "https://auth-server/auth/realms/BC", + "tractionTenantId": "your_traction_tenant_id", + "tractionWalletId": "your_traction_wallet_id", + "tractionApiUrl": "https://traction-tenant-proxy-test.apps.silver.devops.gov.bc.ca", + "tractionApiKey": "your_traction_api_key" + }' \ + http://localhost:5005/tenants +``` + +### List All Tenants + +#### PowerShell +```powershell +$response = Invoke-RestMethod ` + -Uri "http://localhost:5005/tenants" ` + -Method Get ` + -Headers @{ + "Authorization" = "Bearer $token" + } + +$response.tenants | Format-Table id, oidcIssuer +``` + +#### cURL +```bash +curl -X GET \ + -H "Authorization: Bearer $token" \ + http://localhost:5005/tenants +``` + +### Get a Specific Tenant + +#### PowerShell +```powershell +$tenantId = "showcase-tenantA" +$response = Invoke-RestMethod ` + -Uri "http://localhost:5005/tenants/$tenantId" ` + -Method Get ` + -Headers @{ + "Authorization" = "Bearer $token" + } + +$response.tenant +``` + +#### cURL +```bash +curl -X GET \ + -H "Authorization: Bearer $token" \ + http://localhost:5005/tenants/showcase-tenantA +``` + +### Update a Tenant + +#### PowerShell +```powershell +$tenantId = "showcase-tenantA" +$tenantData = @{ + id = "showcase-tenantA" + oidcIssuer = "https://auth-server/auth/realms/BC" + tractionTenantId = "updated_traction_tenant_id" + tractionWalletId = "updated_traction_wallet_id" + tractionApiUrl = "https://traction-tenant-proxy-test.apps.silver.devops.gov.bc.ca" + tractionApiKey = "updated_traction_api_key" +} + +$jsonBody = $tenantData | ConvertTo-Json + +$response = Invoke-RestMethod ` + -Uri "http://localhost:5005/tenants/$tenantId" ` + -Method Put ` + -Headers @{ + "Authorization" = "Bearer $token" + "Content-Type" = "application/json" + } ` + -Body $jsonBody +``` + +#### cURL +```bash +curl -X PUT \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "showcase-tenantA", + "oidcIssuer": "https://auth-server/auth/realms/BC", + "tractionTenantId": "updated_traction_tenant_id", + "tractionWalletId": "updated_traction_wallet_id", + "tractionApiUrl": "https://traction-tenant-proxy-test.apps.silver.devops.gov.bc.ca", + "tractionApiKey": "updated_traction_api_key" + }' \ + http://localhost:5005/tenants/showcase-tenantA +``` + +### Delete a Tenant + +#### PowerShell +```powershell +$tenantId = "showcase-tenantA" +$response = Invoke-RestMethod ` + -Uri "http://localhost:5005/tenants/$tenantId" ` + -Method Delete ` + -Headers @{ + "Authorization" = "Bearer $token" + } +``` + +#### cURL +```bash +curl -X DELETE \ + -H "Authorization: Bearer $token" \ + http://localhost:5005/tenants/showcase-tenantA +``` + +### Error Handling + +For better error handling in PowerShell, you can use try/catch blocks: + +```powershell +try { + $response = Invoke-RestMethod ` + -Uri "http://localhost:5005/tenants" ` + -Method Get ` + -Headers @{ + "Authorization" = "Bearer $token" + } +} +catch { + $statusCode = $_.Exception.Response.StatusCode.value__ + $errorMessage = $_.ErrorDetails.Message + Write-Host "Error: $statusCode - $errorMessage" +} +``` \ No newline at end of file diff --git a/apps/bc-wallet-api-server/drizzle.config.ts b/apps/bc-wallet-api-server/drizzle.config.ts new file mode 100644 index 000000000..09b758b3f --- /dev/null +++ b/apps/bc-wallet-api-server/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'drizzle-kit' + +const dbUrl = + process.env.DB_URL ?? + `postgresql://${process.env.SHOWCASE_DB_USERNAME}${process.env.SHOWCASE_DB_PASSWORD && `:${process.env.SHOWCASE_DB_PASSWORD}`}${process.env.SHOWCASE_DB_HOST && `@${process.env.SHOWCASE_DB_HOST}`}${process.env.SHOWCASE_DB_PORT && `:${process.env.SHOWCASE_DB_PORT}`}/${process.env.SHOWCASE_DB_NAME}` + +const drizzleConfig = defineConfig({ + out: './src/database/migrations', + schema: './src/database/schema/*', + dialect: 'postgresql', + dbCredentials: { + url: dbUrl, + }, +}) + +export default drizzleConfig diff --git a/apps/bc-wallet-api-server/package.json b/apps/bc-wallet-api-server/package.json new file mode 100644 index 000000000..485a5785a --- /dev/null +++ b/apps/bc-wallet-api-server/package.json @@ -0,0 +1,57 @@ +{ + "name": "bc-wallet-api-server", + "version": "0.1.0", + "source": "src/index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "start": "ts-node src/index.ts", + "dev": "ts-node-dev --transpile-only src/index.ts", + "build": "tsc", + "build:clean": "tsc --build --clean && tsc --build", + "migration:generate": "npx drizzle-kit generate --config=drizzle.config.ts --name=bc-wallet-api" + }, + "dependencies": { + "bc-wallet-adapter-client-api": "workspace:*", + "bc-wallet-openapi": "workspace:*", + "cors": "^2.8.5", + "cross-fetch": "3.1.8", + "dotenv-flow": "^4.1.0", + "drizzle-orm": "^0.39.3", + "express": "^4.21.2", + "nanoid": "3.3.11", + "lru-cache": "^11.0.2", + "pg": "8.14.0", + "reflect-metadata": "^0.2.2", + "routing-controllers": "^0.11.1", + "slugify": "1.6.6", + "supertest": "^7.0.0", + "typedi": "^0.10.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@electric-sql/pglite": "^0.2.17", + "@testcontainers/rabbitmq": "^10.18.0", + "@types/cors": "^2.8.17", + "@types/dotenv-flow": "^3.3.3", + "@types/express": "^5.0.0", + "@types/pg": "^8.11.11", + "drizzle-kit": "^0.30.4", + "testcontainers": "^10.18.0", + + "typescript": "^5.8.3" + }, + "files": [ + "dist/**/*", + "README.md", + "LICENSE" + ], + "private": false, + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:Sphereon-Opensource/bc-wallet-api.git", + "author": "4Sure", + "license": "Apache-2.0", + "keywords": [] +} diff --git a/apps/bc-wallet-api-server/src/controllers/ApprovalController.ts b/apps/bc-wallet-api-server/src/controllers/ApprovalController.ts new file mode 100644 index 000000000..5b239cc47 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/ApprovalController.ts @@ -0,0 +1,79 @@ +import { + CredentialDefinitionResponse, + CredentialDefinitionResponseFromJSONTyped, + PendingApprovalsResponse, + PendingApprovalsResponseFromJSONTyped, + ShowcaseResponse, + ShowcaseResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { Authorized, Get, JsonController, NotFoundError, Param, Post } from 'routing-controllers' +import { Service } from 'typedi' + +import CredentialDefinitionService from '../services/CredentialDefinitionService' +import ShowcaseService from '../services/ShowcaseService' +import { getBasePath } from '../utils/auth' +import { credentialDefinitionDTOFrom, showcaseDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath()) +@Service() +export class ApprovalController { + public constructor( + private showcaseService: ShowcaseService, + private credentialDefinitionService: CredentialDefinitionService, + ) {} + + @Authorized() + @Post('/credentials/definitions/:definitionId/approve') + public async approveCredentialDefinition( + @Param('definitionId') definitionId: string, + ): Promise { + try { + const updatedDefinition = await this.credentialDefinitionService.approveCredentialDefinition(definitionId) + if (!updatedDefinition) { + return Promise.reject(new NotFoundError(`Credential Definition with ID ${definitionId} not found`)) + } + return CredentialDefinitionResponseFromJSONTyped( + { credentialDefinition: credentialDefinitionDTOFrom(updatedDefinition) }, + false, + ) + } catch (e) { + console.error(`Approve credential definition id=${definitionId} failed:`, e) + return Promise.reject(e) + } + } + + @Authorized() + @Post('/showcases/:slug/approve') + public async approveShowcase(@Param('slug') slug: string): Promise { + try { + const updatedShowcase = await this.showcaseService.approveShowcaseBySlug(slug) + if (!updatedShowcase) { + return Promise.reject(new NotFoundError(`Showcase with slug ${slug} not found`)) + } + return ShowcaseResponseFromJSONTyped({ showcase: showcaseDTOFrom(updatedShowcase) }, false) + } catch (e) { + console.error(`Approve showcase slug=${slug} failed:`, e) + return Promise.reject(e) + } + } + + @Get('/approvals/pending') + public async getPendingApprovals(): Promise { + try { + const [pendingShowcases, pendingDefinitions] = await Promise.all([ + this.showcaseService.getUnapproved(), + this.credentialDefinitionService.getUnapproved(), + ]) + + const showcases = pendingShowcases.map(showcaseDTOFrom) + const credentialDefinitions = pendingDefinitions.map(credentialDefinitionDTOFrom) + + return PendingApprovalsResponseFromJSONTyped({ showcases, credentialDefinitions }, false) + } catch (e) { + console.error('Get pending approvals failed:', e) + return Promise.reject(e) + } + } +} + +export default ApprovalController diff --git a/apps/bc-wallet-api-server/src/controllers/AssetController.ts b/apps/bc-wallet-api-server/src/controllers/AssetController.ts new file mode 100644 index 000000000..939c5b825 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/AssetController.ts @@ -0,0 +1,124 @@ +import { + AssetResponse, + AssetResponseFromJSONTyped, + AssetRequest, + AssetsResponse, + AssetsResponseFromJSONTyped, + instanceOfAssetRequest, +} from 'bc-wallet-openapi' +import { Response } from 'express' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, + Res, +} from 'routing-controllers' +import { Service } from 'typedi' + +import AssetService from '../services/AssetService' +import { getBasePath } from '../utils/auth' +import { assetDTOFrom, newAssetFrom } from '../utils/mappers' + +@JsonController(getBasePath('/assets')) +@Service() +class AssetController { + public constructor(private assetService: AssetService) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.assetService.getAssets() + const assets = result.map((asset) => assetDTOFrom(asset)) + return AssetsResponseFromJSONTyped({ assets }, false) + } catch (e) { + console.error(`getAll failed:`, e) + return Promise.reject(e) + } + } + + @Get('/:id') + public async getOne(@Param('id') id: string): Promise { + try { + const result = await this.assetService.getAsset(id) + return AssetResponseFromJSONTyped({ asset: assetDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`getOne id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post(@Body({ options: { limit: '250mb' } }) assetRequest: AssetRequest): Promise { + try { + if (!instanceOfAssetRequest(assetRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.assetService.createAsset(newAssetFrom(assetRequest)) + return AssetResponseFromJSONTyped({ asset: assetDTOFrom(result) }, false) + } catch (e) { + console.error(`post failed:`, e) + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:id') + public async put( + @Param('id') id: string, + @Body({ options: { limit: '250mb' } }) assetRequest: AssetRequest, + ): Promise { + try { + if (!instanceOfAssetRequest(assetRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.assetService.updateAsset(id, newAssetFrom(assetRequest)) + return AssetResponseFromJSONTyped({ asset: assetDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`put id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:id') + public async delete(@Param('id') id: string): Promise { + try { + return await this.assetService.deleteAsset(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`delete id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:id/file') + public async getAssetAsFile(@Param('id') id: string, @Res() res: Response): Promise { + try { + const result = await this.assetService.getAsset(id) + res.setHeader('Content-Type', result.mediaType) + return res.send(Buffer.from(result.content.toString(), 'base64')) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`getAssetAsFile id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} +export default AssetController diff --git a/apps/bc-wallet-api-server/src/controllers/CredentialDefinitionController.ts b/apps/bc-wallet-api-server/src/controllers/CredentialDefinitionController.ts new file mode 100644 index 000000000..5c400fd2d --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/CredentialDefinitionController.ts @@ -0,0 +1,150 @@ +import { + CredentialDefinitionImportRequest, + CredentialDefinitionImportRequestToJSONTyped, + CredentialDefinitionRequest, + CredentialDefinitionRequestToJSONTyped, + CredentialDefinitionResponse, + CredentialDefinitionResponseFromJSONTyped, + CredentialDefinitionsResponse, + CredentialDefinitionsResponseFromJSONTyped, + instanceOfCredentialDefinitionImportRequest, + instanceOfCredentialDefinitionRequest, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import CredentialDefinitionService from '../services/CredentialDefinitionService' +import { getBasePath } from '../utils/auth' +import { credentialDefinitionDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/credentials/definitions')) +@Service() +export class CredentialDefinitionController { + public constructor(private credentialDefinitionService: CredentialDefinitionService) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.credentialDefinitionService.getCredentialDefinitions() + const credentialDefinitions = result.map((credentialDefinition) => + credentialDefinitionDTOFrom(credentialDefinition), + ) + return CredentialDefinitionsResponseFromJSONTyped({ credentialDefinitions }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error('getAll definitions failed:', e) + } + return Promise.reject(e) + } + } + + @Get('/:id') + public async getOne(@Param('id') id: string): Promise { + try { + const result = await this.credentialDefinitionService.getCredentialDefinition(id) + return CredentialDefinitionResponseFromJSONTyped( + { credentialDefinition: credentialDefinitionDTOFrom(result) }, + false, + ) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`getOne definition id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post( + @Body() credentialDefinitionRequest: CredentialDefinitionRequest, + ): Promise { + try { + if (!instanceOfCredentialDefinitionRequest(credentialDefinitionRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.credentialDefinitionService.createCredentialDefinition( + CredentialDefinitionRequestToJSONTyped(credentialDefinitionRequest), + ) + return CredentialDefinitionResponseFromJSONTyped( + { credentialDefinition: credentialDefinitionDTOFrom(result) }, + false, + ) + } catch (e) { + console.error('credentialDefinitionRequest post failed:', e) + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Post('/import') + public async importCredentialDefinition( + @Body() credentialDefinitionRequest: CredentialDefinitionImportRequest, + ): Promise { + try { + if (!instanceOfCredentialDefinitionImportRequest(credentialDefinitionRequest)) { + return Promise.reject(new BadRequestError()) + } + await this.credentialDefinitionService.importCredentialDefinition( + CredentialDefinitionImportRequestToJSONTyped(credentialDefinitionRequest), + ) + } catch (e) { + console.error('importCredentialDefinition failed:', e) + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:id') + public async put( + @Param('id') id: string, + @Body() credentialDefinitionRequest: CredentialDefinitionRequest, + ): Promise { + try { + if (!instanceOfCredentialDefinitionRequest(credentialDefinitionRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.credentialDefinitionService.updateCredentialDefinition( + id, + CredentialDefinitionRequestToJSONTyped(credentialDefinitionRequest), + ) + return CredentialDefinitionResponseFromJSONTyped( + { credentialDefinition: credentialDefinitionDTOFrom(result) }, + false, + ) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`put definition id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:id') + public async delete(@Param('id') id: string): Promise { + try { + return this.credentialDefinitionService.deleteCredentialDefinition(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`delete definition id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} diff --git a/apps/bc-wallet-api-server/src/controllers/CredentialSchemaController.ts b/apps/bc-wallet-api-server/src/controllers/CredentialSchemaController.ts new file mode 100644 index 000000000..ecdb8cf27 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/CredentialSchemaController.ts @@ -0,0 +1,132 @@ +import { + CredentialSchemaImportRequest, + CredentialSchemaRequest, + CredentialSchemaRequestToJSONTyped, + CredentialSchemaResponse, + CredentialSchemaResponseFromJSONTyped, + CredentialSchemasResponse, + CredentialSchemasResponseFromJSONTyped, + instanceOfCredentialSchemaImportRequest, + instanceOfCredentialSchemaRequest, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import CredentialSchemaService from '../services/CredentialSchemaService' +import { getBasePath } from '../utils/auth' + +@JsonController(getBasePath('/credentials/schemas')) +@Service() +export class CredentialSchemaController { + public constructor(private readonly credentialSchemaService: CredentialSchemaService) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.credentialSchemaService.getCredentialSchemas() + return CredentialSchemasResponseFromJSONTyped({ credentialSchemas: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error('getAll schemas failed:', e) + } + return Promise.reject(e) + } + } + + @Get('/:id') + public async getOne(@Param('id') id: string): Promise { + try { + const credentialSchema = await this.credentialSchemaService.getCredentialSchema(id) + return CredentialSchemaResponseFromJSONTyped({ credentialSchema }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`getOne schema id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post(@Body() credentialSchemaRequest: CredentialSchemaRequest): Promise { + try { + if (!instanceOfCredentialSchemaRequest(credentialSchemaRequest)) { + return Promise.reject(new BadRequestError()) + } + const credentialSchema = await this.credentialSchemaService.createCredentialSchema( + CredentialSchemaRequestToJSONTyped(credentialSchemaRequest), + ) + return CredentialSchemaResponseFromJSONTyped({ credentialSchema }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error('credentialSchemaRequest post failed:', e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Post('/import') + public async importSchema(@Body() importRequest: CredentialSchemaImportRequest): Promise { + try { + if (!instanceOfCredentialSchemaImportRequest(importRequest)) { + return Promise.reject(new BadRequestError()) + } + await this.credentialSchemaService.importCredentialSchema(importRequest) + } catch (e) { + console.error('credentialSchemaRequest import failed:', e) + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:id') + public async put( + @Param('id') id: string, + @Body() credentialSchemaRequest: CredentialSchemaRequest, + ): Promise { + try { + if (!instanceOfCredentialSchemaRequest(credentialSchemaRequest)) { + return Promise.reject(new BadRequestError()) + } + const credentialSchema = await this.credentialSchemaService.updateCredentialSchema( + id, + CredentialSchemaRequestToJSONTyped(credentialSchemaRequest), + ) + return CredentialSchemaResponseFromJSONTyped({ credentialSchema }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`put schema id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:id') + public async delete(@Param('id') id: string): Promise { + try { + return this.credentialSchemaService.deleteCredentialSchema(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`delete schema id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} diff --git a/apps/bc-wallet-api-server/src/controllers/IssuanceScenarioController.ts b/apps/bc-wallet-api-server/src/controllers/IssuanceScenarioController.ts new file mode 100644 index 000000000..8f0a2c2ce --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/IssuanceScenarioController.ts @@ -0,0 +1,358 @@ +import { + IssuanceScenarioResponse, + IssuanceScenarioResponseFromJSONTyped, + IssuanceScenariosResponse, + IssuanceScenariosResponseFromJSONTyped, + StepsResponse, + StepsResponseFromJSONTyped, + StepResponse, + StepResponseFromJSONTyped, + StepRequest, + StepRequestToJSONTyped, + StepActionsResponse, + StepActionsResponseFromJSONTyped, + StepActionResponse, + StepActionResponseFromJSONTyped, + StepActionRequest, + StepActionRequestToJSONTyped, + IssuanceScenarioRequest, + IssuanceScenarioRequestToJSONTyped, + instanceOfIssuanceScenarioRequest, + instanceOfStepRequest, + instanceOfStepActionRequest, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import ScenarioService from '../services/ScenarioService' +import { IssuanceScenario, ScenarioType } from '../types' +import { getBasePath } from '../utils/auth' +import { issuanceScenarioDTOFrom, stepDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/scenarios/issuances')) +@Service() +class IssuanceScenarioController { + public constructor(private scenarioService: ScenarioService) {} + + @Get('/') + public async getAllIssuanceScenarios(): Promise { + try { + const result = (await this.scenarioService.getScenarios({ + filter: { scenarioType: ScenarioType.ISSUANCE }, + })) as IssuanceScenario[] + const issuanceScenarios = result.map((issuanceScenario) => issuanceScenarioDTOFrom(issuanceScenario)) + return IssuanceScenariosResponseFromJSONTyped({ issuanceScenarios }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all issuance scenarios failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug') + public async getOneIssuanceScenario(@Param('slug') slug: string): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = (await this.scenarioService.getScenario(issuanceScenarioId)) as IssuanceScenario + return IssuanceScenarioResponseFromJSONTyped({ issuanceScenario: issuanceScenarioDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async postIssuanceScenario( + @Body() issuanceScenarioRequest: IssuanceScenarioRequest, + ): Promise { + try { + if (!instanceOfIssuanceScenarioRequest(issuanceScenarioRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = (await this.scenarioService.createScenario( + IssuanceScenarioRequestToJSONTyped(issuanceScenarioRequest), + )) as IssuanceScenario + return IssuanceScenarioResponseFromJSONTyped({ issuanceScenario: issuanceScenarioDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create issuance scenario failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug') + public async putIssuanceScenario( + @Param('slug') slug: string, + @Body() issuanceScenarioRequest: IssuanceScenarioRequest, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfIssuanceScenarioRequest(issuanceScenarioRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = (await this.scenarioService.updateScenario( + issuanceScenarioId, + IssuanceScenarioRequestToJSONTyped(issuanceScenarioRequest), + )) as IssuanceScenario + return IssuanceScenarioResponseFromJSONTyped({ issuanceScenario: issuanceScenarioDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug') + public async deleteIssuanceScenario(@Param('slug') slug: string): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + return await this.scenarioService.deleteScenario(issuanceScenarioId) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps') + public async getAllSteps(@Param('slug') slug: string): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioSteps(issuanceScenarioId) + const steps = result.map((step) => stepDTOFrom(step)) + return StepsResponseFromJSONTyped({ steps }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all steps for issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps/:stepId') + public async getOneIssuanceScenarioStep( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioStep(issuanceScenarioId, stepId) + return StepResponseFromJSONTyped({ step: stepDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get step id=${stepId} for issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/:slug/steps') + public async postIssuanceScenarioStep( + @Param('slug') slug: string, + @Body() stepRequest: StepRequest, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepRequest(stepRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.createScenarioStep( + issuanceScenarioId, + StepRequestToJSONTyped(stepRequest), + ) + return StepResponseFromJSONTyped({ step: stepDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create step for issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug/steps/:stepId') + public async putIssuanceScenarioStep( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Body() stepRequest: StepRequest, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepRequest(stepRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.updateScenarioStep( + issuanceScenarioId, + stepId, + StepRequestToJSONTyped(stepRequest), + ) + return StepResponseFromJSONTyped({ step: stepDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update step id=${stepId} for issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug/steps/:stepId') + public async deleteIssuanceScenarioStep(@Param('slug') slug: string, @Param('stepId') stepId: string): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + return this.scenarioService.deleteScenarioStep(issuanceScenarioId, stepId) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete step id=${stepId} for issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps/:stepId/actions') + public async getAllIssuanceScenarioStepActions( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioStepActions(issuanceScenarioId, stepId) + const actions = result.map((action) => action) + return StepActionsResponseFromJSONTyped({ actions }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all actions for step id=${stepId}, issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps/:stepId/actions/:actionId') + public async getOneIssuanceScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Param('actionId') actionId: string, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioStepAction(issuanceScenarioId, stepId, actionId) + return StepActionResponseFromJSONTyped({ action: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Get action id=${actionId} for step id=${stepId}, issuance scenario id=${issuanceScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/:slug/steps/:stepId/actions') + public async postIssuanceScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Body() actionRequest: StepActionRequest, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepActionRequest(actionRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.createScenarioStepAction( + issuanceScenarioId, + stepId, + StepActionRequestToJSONTyped(actionRequest), + ) + return StepActionResponseFromJSONTyped({ action: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create action for step id=${stepId}, issuance scenario id=${issuanceScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug/steps/:stepId/actions/:actionId') + public async putIssuanceScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Param('actionId') actionId: string, + @Body() actionRequest: StepActionRequest, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepActionRequest(actionRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.updateScenarioStepAction( + issuanceScenarioId, + stepId, + actionId, + StepActionRequestToJSONTyped(actionRequest), + ) + return StepActionResponseFromJSONTyped({ action: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Update action id=${actionId} for step id=${stepId}, issuance scenario id=${issuanceScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug/steps/:stepId/actions/:actionId') + public async deleteIssuanceScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Param('actionId') actionId: string, + ): Promise { + const issuanceScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + return this.scenarioService.deleteScenarioStepAction(issuanceScenarioId, stepId, actionId) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Delete action id=${actionId} for step id=${stepId}, issuance scenario id=${issuanceScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } +} + +export default IssuanceScenarioController diff --git a/apps/bc-wallet-api-server/src/controllers/IssuerController.ts b/apps/bc-wallet-api-server/src/controllers/IssuerController.ts new file mode 100644 index 000000000..f5a215ba0 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/IssuerController.ts @@ -0,0 +1,124 @@ +import { + instanceOfIssuerRequest, + IssuerRequest, + IssuerRequestToJSONTyped, + IssuerResponse, + IssuerResponseFromJSONTyped, + IssuersResponse, + IssuersResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import IssuerService from '../services/IssuerService' +import { getBasePath } from '../utils/auth' +import { issuerDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/roles/issuers')) +@Service() +class IssuerController { + public constructor(private issuerService: IssuerService) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.issuerService.getIssuers() + const issuers = result.map((issuer) => issuerDTOFrom(issuer)) + return IssuersResponseFromJSONTyped({ issuers }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all issuers failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:id') + public async getOne(@Param('id') id: string): Promise { + try { + const result = await this.issuerService.getIssuer(id) + return IssuerResponseFromJSONTyped({ issuer: issuerDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get issuer id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post(@Body() issuerRequest: IssuerRequest): Promise { + try { + if (!instanceOfIssuerRequest(issuerRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.issuerService.createIssuer(IssuerRequestToJSONTyped(issuerRequest)) + return IssuerResponseFromJSONTyped({ issuer: issuerDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create issuer failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:id') + public async put(@Param('id') id: string, @Body() issuerRequest: IssuerRequest): Promise { + try { + if (!instanceOfIssuerRequest(issuerRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.issuerService.updateIssuer(id, IssuerRequestToJSONTyped(issuerRequest)) + return IssuerResponseFromJSONTyped({ issuer: issuerDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update issuer id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Post('/:id/publish') + public async publish(@Param('id') id: string): Promise { + try { + await this.issuerService.publishIssuer(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update issuer id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:id') + public async delete(@Param('id') id: string): Promise { + try { + return this.issuerService.deleteIssuer(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete issuer id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} + +export default IssuerController diff --git a/apps/bc-wallet-api-server/src/controllers/PersonaController.ts b/apps/bc-wallet-api-server/src/controllers/PersonaController.ts new file mode 100644 index 000000000..93f5d836c --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/PersonaController.ts @@ -0,0 +1,110 @@ +import { + instanceOfPersonaRequest, + PersonaRequest, + PersonaRequestToJSONTyped, + PersonaResponse, + PersonaResponseFromJSONTyped, + PersonasResponse, + PersonasResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import PersonaService from '../services/PersonaService' +import { getBasePath } from '../utils/auth' +import { personaDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/personas')) +@Service() +class PersonaController { + public constructor(private personaService: PersonaService) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.personaService.getAll() + const personas = result.map((persona) => personaDTOFrom(persona)) + return PersonasResponseFromJSONTyped({ personas }, false) + } catch (e) { + console.error(`getAll failed:`, e) + return Promise.reject(e) + } + } + + @Get('/:slug') + public async get(@Param('slug') slug: string): Promise { + const id = await this.personaService.getIdBySlug(slug) + try { + const result = await this.personaService.get(id) + return PersonaResponseFromJSONTyped({ persona: personaDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`get id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post(@Body() personaRequest: PersonaRequest): Promise { + try { + if (!instanceOfPersonaRequest(personaRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.personaService.create(PersonaRequestToJSONTyped(personaRequest)) + return PersonaResponseFromJSONTyped({ persona: personaDTOFrom(result) }, false) + } catch (e) { + console.error(`post failed:`, e) + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug') + public async put(@Param('slug') slug: string, @Body() personaRequest: PersonaRequest): Promise { + const id = await this.personaService.getIdBySlug(slug) + try { + if (!instanceOfPersonaRequest(personaRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.personaService.update(id, PersonaRequestToJSONTyped(personaRequest)) + return PersonaResponseFromJSONTyped({ persona: personaDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`put id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug') + public async delete(@Param('slug') slug: string): Promise { + const id = await this.personaService.getIdBySlug(slug) + try { + return await this.personaService.delete(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`delete id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} + +export default PersonaController diff --git a/apps/bc-wallet-api-server/src/controllers/PresentationScenarioController.ts b/apps/bc-wallet-api-server/src/controllers/PresentationScenarioController.ts new file mode 100644 index 000000000..a37e041f8 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/PresentationScenarioController.ts @@ -0,0 +1,378 @@ +import { + instanceOfPresentationScenarioRequest, + instanceOfStepActionRequest, + instanceOfStepRequest, + PresentationScenarioRequest, + PresentationScenarioRequestToJSONTyped, + PresentationScenarioResponse, + PresentationScenarioResponseFromJSONTyped, + PresentationScenariosResponse, + PresentationScenariosResponseFromJSONTyped, + StepActionRequest, + StepActionRequestToJSONTyped, + StepActionResponse, + StepActionResponseFromJSONTyped, + StepActionsResponse, + StepActionsResponseFromJSONTyped, + StepRequest, + StepRequestToJSONTyped, + StepResponse, + StepResponseFromJSONTyped, + StepsResponse, + StepsResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import ScenarioService from '../services/ScenarioService' +import { PresentationScenario, ScenarioType } from '../types' +import { getBasePath } from '../utils/auth' +import { presentationScenarioDTOFrom, stepDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/scenarios/presentations')) +@Service() +class PresentationScenarioController { + public constructor(private scenarioService: ScenarioService) {} + + @Get('/') + public async getAllPresentationScenarios(): Promise { + try { + const result = (await this.scenarioService.getScenarios({ + filter: { scenarioType: ScenarioType.PRESENTATION }, + })) as PresentationScenario[] + const presentationScenarios = result.map((presentationScenario) => + presentationScenarioDTOFrom(presentationScenario), + ) + return PresentationScenariosResponseFromJSONTyped({ presentationScenarios }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all presentation scenarios failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug') + public async getOnePresentationScenario(@Param('slug') slug: string): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = (await this.scenarioService.getScenario(presentationScenarioId)) as PresentationScenario + return PresentationScenarioResponseFromJSONTyped( + { presentationScenario: presentationScenarioDTOFrom(result) }, + false, + ) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async postPresentationScenario( + @Body() presentationScenarioRequest: PresentationScenarioRequest, + ): Promise { + try { + if (!instanceOfPresentationScenarioRequest(presentationScenarioRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = (await this.scenarioService.createScenario( + PresentationScenarioRequestToJSONTyped(presentationScenarioRequest), + )) as PresentationScenario + return PresentationScenarioResponseFromJSONTyped( + { presentationScenario: presentationScenarioDTOFrom(result) }, + false, + ) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create presentation scenario failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug') + public async putPresentationScenario( + @Param('slug') slug: string, + @Body() presentationScenarioRequest: PresentationScenarioRequest, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfPresentationScenarioRequest(presentationScenarioRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = (await this.scenarioService.updateScenario( + presentationScenarioId, + PresentationScenarioRequestToJSONTyped(presentationScenarioRequest), + )) as PresentationScenario + return PresentationScenarioResponseFromJSONTyped( + { presentationScenario: presentationScenarioDTOFrom(result) }, + false, + ) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug') + public async deletePresentationScenario(@Param('slug') slug: string): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + return await this.scenarioService.deleteScenario(presentationScenarioId) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps') + public async getAllSteps(@Param('slug') slug: string): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioSteps(presentationScenarioId) + const steps = result.map((step) => stepDTOFrom(step)) + return StepsResponseFromJSONTyped({ steps }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all steps for presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps/:stepId') + public async getOnePresentationScenarioStep( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioStep(presentationScenarioId, stepId) + return StepResponseFromJSONTyped({ step: stepDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get step id=${stepId} for presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/:slug/steps') + public async postPresentationScenarioStep( + @Param('slug') slug: string, + @Body() stepRequest: StepRequest, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepRequest(stepRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.createScenarioStep( + presentationScenarioId, + StepRequestToJSONTyped(stepRequest), + ) + return StepResponseFromJSONTyped({ step: stepDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create step for presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug/steps/:stepId') + public async putPresentationScenarioStep( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Body() stepRequest: StepRequest, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepRequest(stepRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.updateScenarioStep( + presentationScenarioId, + stepId, + StepRequestToJSONTyped(stepRequest), + ) + return StepResponseFromJSONTyped({ step: stepDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update step id=${stepId} for presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug/steps/:stepId') + public async deletePresentationScenarioStep( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + return this.scenarioService.deleteScenarioStep(presentationScenarioId, stepId) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete step id=${stepId} for presentation scenario id=${presentationScenarioId} failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps/:stepId/actions') + public async getAllPresentationScenarioStepActions( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioStepActions(presentationScenarioId, stepId) + const actions = result.map((action) => action) + return StepActionsResponseFromJSONTyped({ actions }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Get all actions for step id=${stepId}, presentation scenario id=${presentationScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } + + @Get('/:slug/steps/:stepId/actions/:actionId') + public async getOnePresentationScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Param('actionId') actionId: string, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + const result = await this.scenarioService.getScenarioStepAction(presentationScenarioId, stepId, actionId) + return StepActionResponseFromJSONTyped({ action: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Get action id=${actionId} for step id=${stepId}, presentation scenario id=${presentationScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/:slug/steps/:stepId/actions') + public async postPresentationScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Body() actionRequest: StepActionRequest, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepActionRequest(actionRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.createScenarioStepAction( + presentationScenarioId, + stepId, + StepActionRequestToJSONTyped(actionRequest), + ) + return StepActionResponseFromJSONTyped({ action: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Create action for step id=${stepId}, presentation scenario id=${presentationScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug/steps/:stepId/actions/:actionId') + public async putPresentationScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Param('actionId') actionId: string, + @Body() actionRequest: StepActionRequest, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + if (!instanceOfStepActionRequest(actionRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.scenarioService.updateScenarioStepAction( + presentationScenarioId, + stepId, + actionId, + StepActionRequestToJSONTyped(actionRequest), + ) + return StepActionResponseFromJSONTyped({ action: result }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Update action id=${actionId} for step id=${stepId}, presentation scenario id=${presentationScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug/steps/:stepId/actions/:actionId') + public async deletePresentationScenarioStepAction( + @Param('slug') slug: string, + @Param('stepId') stepId: string, + @Param('actionId') actionId: string, + ): Promise { + const presentationScenarioId = await this.scenarioService.getIdBySlug(slug) + try { + return this.scenarioService.deleteScenarioStepAction(presentationScenarioId, stepId, actionId) + } catch (e) { + if (e.httpCode !== 404) { + console.error( + `Delete action id=${actionId} for step id=${stepId}, presentation scenario id=${presentationScenarioId} failed:`, + e, + ) + } + return Promise.reject(e) + } + } +} + +export default PresentationScenarioController diff --git a/apps/bc-wallet-api-server/src/controllers/RelyingPartyController.ts b/apps/bc-wallet-api-server/src/controllers/RelyingPartyController.ts new file mode 100644 index 000000000..c73f382bd --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/RelyingPartyController.ts @@ -0,0 +1,119 @@ +import { + instanceOfRelyingPartyRequest, + RelyingPartiesResponse, + RelyingPartiesResponseFromJSONTyped, + RelyingPartyRequest, + RelyingPartyRequestToJSONTyped, + RelyingPartyResponse, + RelyingPartyResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import RelyingPartyService from '../services/RelyingPartyService' +import { getBasePath } from '../utils/auth' +import { relyingPartyDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/roles/relying-parties')) +@Service() +class RelyingPartyController { + public constructor(private relyingPartyService: RelyingPartyService) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.relyingPartyService.getRelyingParties() + const relyingParties = result.map((relyingParty) => relyingPartyDTOFrom(relyingParty)) + return RelyingPartiesResponseFromJSONTyped({ relyingParties }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all relying parties failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:id') + public async getOne(@Param('id') id: string): Promise { + try { + const result = await this.relyingPartyService.getRelyingParty(id) + return RelyingPartyResponseFromJSONTyped({ relyingParty: relyingPartyDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get relying party id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post(@Body() relyingPartyRequest: RelyingPartyRequest): Promise { + try { + if (!instanceOfRelyingPartyRequest(relyingPartyRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.relyingPartyService.createRelyingParty( + RelyingPartyRequestToJSONTyped(relyingPartyRequest), + ) + return RelyingPartyResponseFromJSONTyped({ relyingParty: relyingPartyDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create relying party failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:id') + public async put( + @Param('id') id: string, + @Body() relyingPartyRequest: RelyingPartyRequest, + ): Promise { + try { + if (!instanceOfRelyingPartyRequest(relyingPartyRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.relyingPartyService.updateRelyingParty( + id, + RelyingPartyRequestToJSONTyped(relyingPartyRequest), + ) + return RelyingPartyResponseFromJSONTyped({ relyingParty: relyingPartyDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update relying party id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:id') + public async delete(@Param('id') id: string): Promise { + try { + return this.relyingPartyService.deleteRelyingParty(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete relying party id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} + +export default RelyingPartyController diff --git a/apps/bc-wallet-api-server/src/controllers/ShowcaseController.ts b/apps/bc-wallet-api-server/src/controllers/ShowcaseController.ts new file mode 100644 index 000000000..dadfc621b --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/ShowcaseController.ts @@ -0,0 +1,139 @@ +import { + instanceOfShowcaseRequest, + ShowcaseRequest, + ShowcaseRequestToJSONTyped, + ShowcaseResponse, + ShowcaseResponseFromJSONTyped, + ShowcasesResponse, + ShowcasesResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { + Authorized, + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import DuplicationShowcaseService from '../services/DuplicationShowcaseService' +import ShowcaseService from '../services/ShowcaseService' +import { getBasePath } from '../utils/auth' +import { showcaseDTOFrom } from '../utils/mappers' + +@JsonController(getBasePath('/showcases')) +@Service() +class ShowcaseController { + public constructor( + private showcaseService: ShowcaseService, + private duplicationShowcaseService: DuplicationShowcaseService, + ) {} + + @Get('/') + public async getAll(): Promise { + try { + const result = await this.showcaseService.getShowcases() + const showcases = result.map((showcase) => showcaseDTOFrom(showcase)) + return ShowcasesResponseFromJSONTyped({ showcases }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all showcases failed:`, e) + } + return Promise.reject(e) + } + } + + @Get('/:slug') + public async getOne(@Param('slug') slug: string): Promise { + const id = await this.showcaseService.getIdBySlug(slug) + try { + const result = await this.showcaseService.getShowcase(id) + return ShowcaseResponseFromJSONTyped({ showcase: showcaseDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get showcase id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/') + public async post( + @Body({ options: { limit: '250mb' } }) showcaseRequest: ShowcaseRequest, + ): Promise { + try { + if (!instanceOfShowcaseRequest(showcaseRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.showcaseService.createShowcase(ShowcaseRequestToJSONTyped(showcaseRequest)) + return ShowcaseResponseFromJSONTyped({ showcase: showcaseDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create showcase failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @Put('/:slug') + public async put( + @Param('slug') slug: string, + @Body({ options: { limit: '250mb' } }) showcaseRequest: ShowcaseRequest, + ): Promise { + const id = await this.showcaseService.getIdBySlug(slug) + try { + if (!instanceOfShowcaseRequest(showcaseRequest)) { + return Promise.reject(new BadRequestError()) + } + const result = await this.showcaseService.updateShowcase(id, ShowcaseRequestToJSONTyped(showcaseRequest)) + return ShowcaseResponseFromJSONTyped({ showcase: showcaseDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update showcase id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @OnUndefined(204) + @Delete('/:slug') + public async delete(@Param('slug') slug: string): Promise { + const id = await this.showcaseService.getIdBySlug(slug) + try { + return this.showcaseService.deleteShowcase(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete showcase id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @Authorized() + @HttpCode(201) + @Post('/:slug/duplicate') + public async duplicate(@Param('slug') slug: string): Promise { + const id = await this.showcaseService.getIdBySlug(slug) + try { + const result = await this.duplicationShowcaseService.duplicateShowcase(id) + return ShowcaseResponseFromJSONTyped({ showcase: showcaseDTOFrom(result) }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Duplicate showcase id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} + +export default ShowcaseController diff --git a/apps/bc-wallet-api-server/src/controllers/TenantController.ts b/apps/bc-wallet-api-server/src/controllers/TenantController.ts new file mode 100644 index 000000000..ec030c99b --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/TenantController.ts @@ -0,0 +1,112 @@ +import { + instanceOfTenantRequest, + TenantRequest, + TenantRequestToJSONTyped, + TenantResponse, + TenantResponseFromJSONTyped, + TenantsResponse, + TenantsResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { + BadRequestError, + Body, + Delete, + Get, + HttpCode, + JsonController, + OnUndefined, + Param, + Post, + Put, +} from 'routing-controllers' +import { Service } from 'typedi' + +import TenantService from '../services/TenantService' +import { TenantType } from '../types' +import { RootTenantAuthorized, SoftTenantAuthorized } from '../utils/auth' + +@JsonController('/tenants') +@Service() +class TenantController { + public constructor(private tenantService: TenantService) {} + + @SoftTenantAuthorized() + @Get('/') + public async getAll(): Promise { + try { + const tenants = await this.tenantService.getTenants() + return TenantsResponseFromJSONTyped({ tenants }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get all tenants failed:`, e) + } + return Promise.reject(e) + } + } + + @SoftTenantAuthorized() + @Get('/:id') + public async getOne(@Param('id') id: string): Promise { + try { + const tenant = await this.tenantService.getTenant(id) + return TenantResponseFromJSONTyped({ tenant }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Get tenant id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @RootTenantAuthorized() + @HttpCode(201) + @Post('/') + public async post(@Body() tenantRequest: TenantRequest): Promise { + try { + if (!instanceOfTenantRequest(tenantRequest)) { + return Promise.reject(new BadRequestError()) + } + const newTenant = TenantRequestToJSONTyped(tenantRequest) + const tenant = await this.tenantService.createTenant({ ...newTenant, tenantType: TenantType.SHOWCASE }) // Do not allow ROOT tenant to be created externally + return TenantResponseFromJSONTyped({ tenant }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Create tenant failed:`, e) + } + return Promise.reject(e) + } + } + + @RootTenantAuthorized() + @Put('/:id') + public async put(@Param('id') id: string, @Body() tenantRequest: TenantRequest): Promise { + try { + if (!instanceOfTenantRequest(tenantRequest)) { + return Promise.reject(new BadRequestError()) + } + const tenant = await this.tenantService.updateTenant(id, TenantRequestToJSONTyped(tenantRequest)) + return TenantResponseFromJSONTyped({ tenant }, false) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Update tenant id=${id} failed:`, e) + } + return Promise.reject(e) + } + } + + @RootTenantAuthorized() + @OnUndefined(204) + @Delete('/:id') + public async delete(@Param('id') id: string): Promise { + try { + return await this.tenantService.deleteTenant(id) + } catch (e) { + if (e.httpCode !== 404) { + console.error(`Delete tenant id=${id} failed:`, e) + } + return Promise.reject(e) + } + } +} + +export default TenantController diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/ApprovalController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/ApprovalController.integration.test.ts new file mode 100644 index 000000000..b33e8680d --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/ApprovalController.integration.test.ts @@ -0,0 +1,248 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { + CredentialDefinitionResponse, + CredentialDefinitionResponseFromJSONTyped, + PendingApprovalsResponse, + PendingApprovalsResponseFromJSONTyped, + ShowcaseFromJSONTyped, + ShowcaseResponse, + ShowcaseResponseFromJSONTyped, +} from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createTestShowcase, + createTestTenant, + createTestUser, + createUnapprovedCredDef, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import IssuerRepository from '../../database/repositories/IssuerRepository' +import PersonaRepository from '../../database/repositories/PersonaRepository' +import RelyingPartyRepository from '../../database/repositories/RelyingPartyRepository' +import ScenarioRepository from '../../database/repositories/ScenarioRepository' +import ShowcaseRepository from '../../database/repositories/ShowcaseRepository' +import TenantRepository from '../../database/repositories/TenantRepository' +import UserRepository from '../../database/repositories/UserRepository' +import CredentialDefinitionService from '../../services/CredentialDefinitionService' +import ShowcaseService from '../../services/ShowcaseService' +import TenantService from '../../services/TenantService' +import { AcceptCredentialAction, ShowcaseStatus } from '../../types' +import ApprovalController from '../ApprovalController' +import { CredentialDefinitionController } from '../CredentialDefinitionController' +import ShowcaseController from '../ShowcaseController' +import { createApiFullTestData } from './apiTestData' +import { registerMockServicesByInterface, setupRabbitMQ, setupTestDatabase } from './globalTestSetup' +import { MockSessionService } from './MockSessionService' +import supertest = require('supertest') + +const isRecentDate = (inputDate: Date | null | undefined, seconds = 5): boolean => { + if (!inputDate) return false + const now = new Date() + const diff = now.getTime() - inputDate.getTime() + return diff >= 0 && diff < seconds * 1000 +} + +describe('ApprovalController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let sessionService: MockSessionService + + beforeAll(async () => { + await setupRabbitMQ() + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + + // Register all necessary repositories and services + await registerMockServicesByInterface(database) + useContainer(Container) + sessionService = Container.get(MockSessionService) + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(IssuerRepository) + Container.get(PersonaRepository) + Container.get(RelyingPartyRepository) + Container.get(ScenarioRepository) + Container.get(ShowcaseRepository) + Container.get(UserRepository) + Container.get(TenantRepository) + Container.get(TenantService) + Container.get(CredentialDefinitionService) + Container.get(ShowcaseService) + + sessionService.setCurrentUser(await createTestUser('approver-user-api')) + + // Create a test tenant + sessionService.setCurrentTenant(await createTestTenant()) + + // Setup Express Server with the controller + app = createExpressServer({ + controllers: [ApprovalController, ShowcaseController, CredentialDefinitionController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should approve a specific credential definition', async () => { + // 1. Setup: Create an unapproved definition using the API helper + const definition = await createUnapprovedCredDef('example_name', sessionService.getCurrentTenant()!.id) + + // 2. Action: Call the API endpoint + const response = await request.post(`/credentials/definitions/${definition.id}/approve`).expect(200) + + // 3. Assertions: Check the API response body (OpenAPI type) + const responseBody: CredentialDefinitionResponse = CredentialDefinitionResponseFromJSONTyped(response.body, false) + expect(responseBody).toHaveProperty('credentialDefinition') + const approvedDefResp = responseBody.credentialDefinition + if (!approvedDefResp) { + throw Error(`approvedDefResp is undefined`) + } + expect(approvedDefResp.id).toEqual(definition.id) + expect(approvedDefResp.name).toEqual(definition.name) + expect(approvedDefResp.approvedBy).toBeDefined() + expect(approvedDefResp.approvedBy?.id).toEqual((await sessionService.getCurrentUser())?.id) + expect(approvedDefResp.approvedAt).toBeDefined() + expect(isRecentDate(approvedDefResp.approvedAt)).toBe(true) + expect(approvedDefResp.icon).toBeDefined() + expect(approvedDefResp.credentialSchema).toBeDefined() + }) + + it('should return 404 when approving a non-existent credential definition', async () => { + const nonExistentId = (await sessionService.getCurrentUser())?.id + await request.post(`/credentials/definitions/${nonExistentId}/approve`).expect(404) + }) + + it('should approve a specific showcase', async () => { + const tenantId = getTenantId() + + // 1. Setup: Create an unapproved showcase using the API helper + const showcase = await createTestShowcase(tenantId, 'showcase-api-approve', ShowcaseStatus.PENDING) + + // 2. Action: Call the approval endpoints + const action = showcase.scenarios![0].steps![0].actions![0] as AcceptCredentialAction + await request.post(`/credentials/definitions/${action.credentialDefinitionId}/approve`).expect(200) + const response = await request.post(`/showcases/${showcase.slug}/approve`).expect(200) + + // 3. Assertions: Check the API response body (OpenAPI type) + const responseBody: ShowcaseResponse = ShowcaseResponseFromJSONTyped(response.body, false) + expect(responseBody).toHaveProperty('showcase') + const approvedShowcaseResp = responseBody.showcase + if (!approvedShowcaseResp) { + throw Error(`approvedShowcaseResp is undefined`) + } + expect(approvedShowcaseResp.id).toEqual(showcase.id) + expect(approvedShowcaseResp.name).toEqual(showcase.name) + expect(approvedShowcaseResp.slug).toEqual(showcase.slug) + expect(approvedShowcaseResp.approvedBy).toBeDefined() // Placeholder ID string from service + // expect(approvedShowcaseResp.approvedBy).toEqual(testUserId); // <<< Use when auth passes real ID + expect(approvedShowcaseResp.approvedAt).toBeDefined() + expect(isRecentDate(approvedShowcaseResp.approvedAt)).toBe(true) + expect(approvedShowcaseResp.bannerImage).toBeDefined() + expect(approvedShowcaseResp.scenarios).toBeInstanceOf(Array) + expect(approvedShowcaseResp.personas).toBeInstanceOf(Array) + expect(approvedShowcaseResp.tenantId).toEqual(tenantId) + }) + + it('should return 404 when approving a non-existent showcase', async () => { + const nonExistentSlug = 'non-existent-showcase-slug-api' + await request.post(`/showcases/${nonExistentSlug}/approve`).expect(404) + }) + + it('should get all unapproved showcases and credential definitions', async () => { + const tenantId = getTenantId() + + // 1. Setup: Create a mix of approved and unapproved items using API helpers + const unapprovedDef = await createUnapprovedCredDef('example_name', tenantId) + const unapprovedShowcase = await createTestShowcase(tenantId, 'pending-showcase-api') + + const approvedDef = await createUnapprovedCredDef('approved-def-api', tenantId) + // Approve directly using service (simulating prior approval) + const credDefService = Container.get(CredentialDefinitionService) + await credDefService.approveCredentialDefinition(approvedDef.id) // Assumes service uses placeholder user + + const approvedShowcase = await createTestShowcase(tenantId, 'approved-showcase-api') + // Approve directly using service (simulating prior approval) + const showcaseService = Container.get(ShowcaseService) + await showcaseService.approveShowcase(approvedShowcase.id) // Assumes service uses placeholder user + + // 2. Action: Call the API endpoint + const response = await request.get('/approvals/pending').expect(200) + + // 3. Assertions: Check the API response body (OpenAPI type) + const responseBody: PendingApprovalsResponse = PendingApprovalsResponseFromJSONTyped(response.body, false) + expect(responseBody).toHaveProperty('credentialDefinitions') + expect(responseBody).toHaveProperty('showcases') + expect(responseBody.credentialDefinitions).toBeInstanceOf(Array) + expect(responseBody.showcases).toBeInstanceOf(Array) + + // Check counts (adjust if other tests leave unapproved items) + // For more robustness, check specifically for the IDs created in *this* test + expect(responseBody.credentialDefinitions?.length).toBeGreaterThanOrEqual(1) + expect(responseBody.showcases?.length).toBeGreaterThanOrEqual(1) + + // Check content of unapproved items + const foundUnapprovedDef = responseBody.credentialDefinitions?.find((def) => def.id === unapprovedDef.id) + expect(foundUnapprovedDef).toBeDefined() + expect(foundUnapprovedDef?.name).toEqual(unapprovedDef.name) + expect(foundUnapprovedDef?.approvedBy).toBeUndefined() + expect(foundUnapprovedDef?.approvedAt).toBeUndefined() + + const foundUnapprovedShowcase = responseBody.showcases?.find((sc) => sc.id === unapprovedShowcase.id) + expect(foundUnapprovedShowcase).toBeDefined() + expect(foundUnapprovedShowcase?.name).toEqual(unapprovedShowcase.name) + expect(foundUnapprovedShowcase?.approvedBy).toBeUndefined() + expect(foundUnapprovedShowcase?.approvedAt).toBeUndefined() + + // Check that approved items are NOT present + const foundApprovedDef = responseBody.credentialDefinitions?.find((def) => def.id === approvedDef.id) + expect(foundApprovedDef).toBeUndefined() + const foundApprovedShowcase = responseBody.showcases?.find((sc) => sc.id === approvedShowcase.id) + expect(foundApprovedShowcase).toBeUndefined() + }) + + function getTenantId(): string { + const tenant = sessionService.getCurrentTenant() + if (!tenant) { + throw Error('No tenant was created that was registered in sessionService') + } + return tenant.id + } + + it('should reject approval of a showcase with unapproved credential definitions', async () => { + const tenantId = getTenantId() + + // 1. Setup: full showcase + const testShowcaseReq = await createApiFullTestData(tenantId) + const testShowcaseResponse = await request.post('/showcases').send(testShowcaseReq.showcaseRequest).expect(201) + const testShowcase = ShowcaseFromJSONTyped(testShowcaseResponse.body.showcase, false) + + // 2. Action: Attempt to approve the showcase without approving the credential definition + const response = await request.post(`/showcases/${testShowcase.slug}/approve`).expect(400) + + // 3. Assertions: Check for the expected error message + expect(response.body.message).toContain(`used by step`) + expect(response.body.message).toContain(`is not approved yet`) + + // Verify the showcase remains unapproved + const pendingResponse = await request.get('/approvals/pending').expect(200) + const pendingBody: PendingApprovalsResponse = PendingApprovalsResponseFromJSONTyped(pendingResponse.body, false) + + // The showcase should still be in the pending list + const stillPendingShowcase = pendingBody.showcases?.find((s) => s.id === testShowcase.id) + expect(stillPendingShowcase).toBeDefined() + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/AssetController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/AssetController.integration.test.ts new file mode 100644 index 000000000..a4c59a24a --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/AssetController.integration.test.ts @@ -0,0 +1,80 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { createMockDatabaseService } from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import AssetService from '../../services/AssetService' +import DatabaseService from '../../services/DatabaseService' +import AssetController from '../AssetController' +import { setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('AssetController Integration Tests', () => { + let app: Application + let request: any + let client: PGlite + + beforeAll(async () => { + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + const mockDatabaseService = await createMockDatabaseService(database) + Container.set(DatabaseService, mockDatabaseService) + useContainer(Container) + Container.get(AssetRepository) + Container.get(AssetService) + app = createExpressServer({ + controllers: [AssetController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + Container.reset() + await client.close() + }) + + it('should create, retrieve, update, and delete an asset', async () => { + const createResponse = await request + .post('/assets') + .send({ + mediaType: 'image/png', + content: 'binary data', + fileName: 'test.png', + description: 'Test asset', + }) + .expect(201) + const created = createResponse.body.asset + expect(created).toHaveProperty('id') + expect(created.fileName).toEqual('test.png') + expect(created.description).toEqual('Test asset') + + const getResponse = await request.get(`/assets/${created.id}`).expect(200) + expect(getResponse.body.asset.fileName).toEqual('test.png') + expect(getResponse.body.asset.description).toEqual('Test asset') + + const allResponse = await request.get('/assets').expect(200) + expect(Array.isArray(allResponse.body.assets)).toBe(true) + expect(allResponse.body.assets.length).toBeGreaterThanOrEqual(1) + + const updateResponse = await request + .put(`/assets/${created.id}`) + .send({ + mediaType: 'image/png', + content: 'binary data', + fileName: 'updated.png', + description: 'Updated asset', + }) + .expect(200) + expect(updateResponse.body.asset.fileName).toEqual('updated.png') + expect(updateResponse.body.asset.description).toEqual('Updated asset') + + await request.delete(`/assets/${created.id}`).expect(204) + await request.get(`/assets/${created.id}`).expect(404) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/CredentialDefinitionController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/CredentialDefinitionController.integration.test.ts new file mode 100644 index 000000000..bbb0ff385 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/CredentialDefinitionController.integration.test.ts @@ -0,0 +1,126 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { CredentialDefinitionRequest, CredentialType, IdentifierType } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createTestAsset, + createTestCredentialSchema, + createTestTenant, + createTestUser, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import TenantRepository from '../../database/repositories/TenantRepository' +import CredentialDefinitionService from '../../services/CredentialDefinitionService' +import TenantService from '../../services/TenantService' +import { CredentialDefinitionController } from '../CredentialDefinitionController' +import { registerMockServicesByInterface, setupRabbitMQ, setupTestDatabase } from './globalTestSetup' +import { MockSessionService } from './MockSessionService' +import supertest = require('supertest') + +describe('CredentialDefinitionController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let tenantId: string + let sessionService: MockSessionService + + beforeAll(async () => { + await setupRabbitMQ() + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + await registerMockServicesByInterface(database) + useContainer(Container) + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(TenantRepository) + Container.get(CredentialDefinitionService) + Container.get(TenantService) + sessionService = Container.get(MockSessionService) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + sessionService.setCurrentTenant(tenant) + sessionService.setCurrentUser(await createTestUser('test-user')) + + app = createExpressServer({ + controllers: [CredentialDefinitionController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a credential definition with tenant association', async () => { + // Create prerequisite: an asset and a credential schema + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + + // Create a credential definition + const createResponse = await request + .post('/credentials/definitions') + .send({ + name: 'Test Credential', + version: '1.0', + identifierType: IdentifierType.Did, + identifier: 'did:test:456', + credentialSchema: credentialSchema.id, + icon: asset.id, + type: CredentialType.Anoncred, + representations: [], + tenantId, + } satisfies CredentialDefinitionRequest) + .expect(201) + const created = createResponse.body.credentialDefinition + expect(created).toHaveProperty('id') + expect(created.credentialSchema).toHaveProperty('id') + expect(created.tenantId).toEqual(tenantId) + + // Retrieve the created definition + const getResponse = await request.get(`/credentials/definitions/${created.id}`).expect(200) + expect(getResponse.body.credentialDefinition.name).toEqual('example_name') + expect(getResponse.body.credentialDefinition.tenantId).toEqual(tenantId) + + // Update the credential definition + const updateResponse = await request + .put(`/credentials/definitions/${created.id}`) + .send({ + name: 'Updated Credential', + version: '1.0', + credentialSchema: credentialSchema.id, + icon: asset.id, + type: CredentialType.Anoncred, + representations: [], + tenantId, + } satisfies CredentialDefinitionRequest) + .expect(200) + expect(updateResponse.body.credentialDefinition.name).toEqual('example_name') + expect(updateResponse.body.credentialDefinition.tenantId).toEqual(tenantId) + + // Delete the credential definition + await request.delete(`/credentials/definitions/${created.id}`).expect(204) + + // Verify deletion (assuming a 404 is returned when not found) + await request.get(`/credentials/definitions/${created.id}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentId = '00000000-0000-0000-0000-000000000000' + + // Try to get a non-existent credential definition + await request.get(`/credentials/definitions/${nonExistentId}`).expect(404) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/CredentialSchemaController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/CredentialSchemaController.integration.test.ts new file mode 100644 index 000000000..45e8d73f7 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/CredentialSchemaController.integration.test.ts @@ -0,0 +1,77 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { CredentialAttributeType, CredentialSchemaRequest, IdentifierType, Source } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { CredentialSchemaController } from '../CredentialSchemaController' +import { registerMockServicesByInterface, setupRabbitMQ, setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('CredentialSchemaController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + + beforeAll(async () => { + await setupRabbitMQ() + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + await registerMockServicesByInterface(database) + useContainer(Container) + app = createExpressServer({ + controllers: [CredentialSchemaController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a credential schema', async () => { + // Create a credential schema + const createResponse = await request + .post('/credentials/schemas') + .send({ + name: 'Test Schema', + version: '1.0', + identifierType: IdentifierType.Did, + identifier: 'did:test:456', + source: Source.Created, + attributes: [{ name: 'attr1', value: 'value1', type: CredentialAttributeType.String }], + } satisfies CredentialSchemaRequest) + .expect(201) + const created = createResponse.body.credentialSchema + expect(created).toHaveProperty('id') + + // Retrieve the created schema + const getResponse = await request.get(`/credentials/schemas/${created.id}`).expect(200) + expect(getResponse.body.credentialSchema.name).toEqual('Test Schema') + + // Update the credential schema + const updateResponse = await request + .put(`/credentials/schemas/${created.id}`) + .send({ + name: 'Updated Schema', + version: '1.0', + identifierType: IdentifierType.Did, + identifier: 'did:test:456', + source: Source.Imported, + attributes: [{ name: 'attr1', value: 'value1', type: CredentialAttributeType.String }], + } satisfies CredentialSchemaRequest) + .expect(200) + expect(updateResponse.body.credentialSchema.name).toEqual('Updated Schema') + + // Delete the credential schema + await request.delete(`/credentials/schemas/${created.id}`).expect(204) + + // Verify deletion (assuming a 404 is returned when not found) + await request.get(`/credentials/schemas/${created.id}`).expect(404) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/IssuanceScenarioController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/IssuanceScenarioController.integration.test.ts new file mode 100644 index 000000000..0a2fb66f3 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/IssuanceScenarioController.integration.test.ts @@ -0,0 +1,276 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { AriesOOBActionRequest } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createMockDatabaseService, + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestIssuer, + createTestPersona, + createTestTenant, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import IssuerRepository from '../../database/repositories/IssuerRepository' +import PersonaRepository from '../../database/repositories/PersonaRepository' +import ScenarioRepository from '../../database/repositories/ScenarioRepository' +import DatabaseService from '../../services/DatabaseService' +import ScenarioService from '../../services/ScenarioService' +import { ScenarioType, StepActionType } from '../../types' +import IssuanceScenarioController from '../IssuanceScenarioController' +import { createApiIssuanceScenarioRequest, createApiStepRequest } from './apiTestData' +import { setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('IssuanceScenarioController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let tenantId: string + + beforeAll(async () => { + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + const mockDatabaseService = await createMockDatabaseService(database) + Container.set(DatabaseService, mockDatabaseService) + useContainer(Container) + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(IssuerRepository) + Container.get(PersonaRepository) + Container.get(ScenarioRepository) + Container.get(ScenarioService) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + app = createExpressServer({ + controllers: [IssuanceScenarioController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete an issuance scenario with steps and actions', async () => { + // Create prerequisites + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + const issuer = await createTestIssuer(asset, credentialDefinition, credentialSchema, tenantId) + const persona = await createTestPersona(asset) + + // 2. Create an issuance scenario - must include at least one step according to the error + const scenarioRequest = createApiIssuanceScenarioRequest(issuer.id, persona.id, asset.id, credentialDefinition.id) + + const createResponse = await request.post('/scenarios/issuances').send(scenarioRequest).expect(201) + + const createdScenario = createResponse.body.issuanceScenario + expect(createdScenario).toHaveProperty('id') + expect(createdScenario.name).toEqual('Test Issuance Scenario') + expect(createdScenario.type).toEqual(ScenarioType.ISSUANCE) + expect(createdScenario.issuer.id).toEqual(issuer.id) + expect(createdScenario.steps.length).toEqual(1) + + // 3. Retrieve all issuance scenarios + const getAllResponse = await request.get('/scenarios/issuances').expect(200) + expect(getAllResponse.body.issuanceScenarios).toBeInstanceOf(Array) + expect(getAllResponse.body.issuanceScenarios.length).toBe(1) + + // 4. Retrieve the created scenario + const getResponse = await request.get(`/scenarios/issuances/${createdScenario.slug}`).expect(200) + expect(getResponse.body.issuanceScenario.name).toEqual('Test Issuance Scenario') + + // 5. Update the scenario + const updateResponse = await request + .put(`/scenarios/issuances/${createdScenario.slug}`) + .send({ + ...scenarioRequest, + name: 'Updated Scenario Name', + }) + .expect(200) + + expect(updateResponse.body.issuanceScenario.name).toEqual('Updated Scenario Name') + const updatedScenario = updateResponse.body.issuanceScenario + + // 6. Create an additional step for the scenario + const stepRequest = createApiStepRequest(asset.id, credentialDefinition.id) + stepRequest.title = 'Additional Step' + stepRequest.description = 'Additional step description' + stepRequest.order = 2 + + const createStepResponse = await request + .post(`/scenarios/issuances/${updatedScenario.slug}/steps`) + .send(stepRequest) + .expect(201) + + const createdStep = createStepResponse.body.step + expect(createdStep).toHaveProperty('id') + expect(createdStep.title).toEqual('Additional Step') + + // 7. Retrieve all steps for the scenario + const getAllStepsResponse = await request.get(`/scenarios/issuances/${updatedScenario.slug}/steps`).expect(200) + expect(getAllStepsResponse.body.steps).toBeInstanceOf(Array) + expect(getAllStepsResponse.body.steps.length).toBe(2) // Now should have 2 steps + + // 8. Retrieve the created step + const getStepResponse = await request + .get(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}`) + .expect(200) + expect(getStepResponse.body.step.title).toEqual('Additional Step') + + // 9. Update the step + const updateStepResponse = await request + .put(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}`) + .send({ + ...stepRequest, + title: 'Updated Step Title', + }) + .expect(200) + + expect(updateStepResponse.body.step.title).toEqual('Updated Step Title') + + // 10. Create an additional action for the step + const actionRequest: AriesOOBActionRequest = { + title: 'Additional Action', + actionType: StepActionType.ARIES_OOB, + text: 'Additional action text', + credentialDefinitionId: credentialDefinition.id, + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + } + + const createActionResponse = await request + .post(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}/actions`) + .send(actionRequest) + .expect(201) + + const createdAction = createActionResponse.body.action + expect(createdAction).toHaveProperty('id') + expect(createdAction.title).toEqual('Additional Action') + expect(createdAction.actionType).toEqual(StepActionType.ARIES_OOB) + + // 11. Retrieve all actions for the step + const getAllActionsResponse = await request + .get(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}/actions`) + .expect(200) + + expect(getAllActionsResponse.body.actions).toBeInstanceOf(Array) + expect(getAllActionsResponse.body.actions.length).toBe(2) // Original action from step creation plus the new one + + // 12. Retrieve the created action + const getActionResponse = await request + .get(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}/actions/${createdAction.id}`) + .expect(200) + + expect(getActionResponse.body.action.title).toEqual('Additional Action') + + // 13. Update the action + const updateActionResponse = await request + .put(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}/actions/${createdAction.id}`) + .send({ + ...actionRequest, + title: 'Updated Action Title', + }) + .expect(200) + + expect(updateActionResponse.body.action.title).toEqual('Updated Action Title') + + // 14. Delete the action + await request + .delete(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}/actions/${createdAction.id}`) + .expect(204) + + // 15. Delete the step + await request.delete(`/scenarios/issuances/${updatedScenario.slug}/steps/${createdStep.id}`).expect(204) + + // 16. Delete the scenario + await request.delete(`/scenarios/issuances/${updatedScenario.slug}`).expect(204) + + // 17. Verify scenario deletion + await request.get(`/scenarios/issuances/${updatedScenario.slug}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentSlug = '00000000-0000-0000-0000-000000000000' + + // Try to get a non-existent scenario + await request.get(`/scenarios/issuances/${nonExistentSlug}`).expect(404) + + // Try to get steps for a non-existent scenario + await request.get(`/scenarios/issuances/${nonExistentSlug}/steps`).expect(404) + + // Try to create a step for a non-existent scenario + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + const stepRequest = createApiStepRequest(asset.id, credentialDefinition.id) + + await request.post(`/scenarios/issuances/${nonExistentSlug}/steps`).send(stepRequest).expect(404) + }) + + it('should validate request data when creating an issuance scenario', async () => { + // Attempt to create a scenario with missing required fields + const invalidScenarioRequest = { + // Missing name, description, etc. + } + + await request.post('/scenarios/issuances').send(invalidScenarioRequest).expect(400) + + // Set up assets and personas for testing + const asset = await createTestAsset() + const persona = await createTestPersona(asset) + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + // Attempt to create a scenario with a non-existent issuer + const nonExistentId = '00000000-0000-0000-0000-000000000000' + const invalidScenarioRequest2 = createApiIssuanceScenarioRequest( + nonExistentId, + persona.id, + asset.id, + credentialDefinition.id, + ) + + await request.post('/scenarios/issuances').send(invalidScenarioRequest2).expect(404) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/IssuerController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/IssuerController.integration.test.ts new file mode 100644 index 000000000..1091b97f5 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/IssuerController.integration.test.ts @@ -0,0 +1,193 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { IssuerRequest } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { createTestAsset, createTestCredentialDefinition, createTestCredentialSchema, createTestTenant } from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import IssuerRepository from '../../database/repositories/IssuerRepository' +import IssuerService from '../../services/IssuerService' +import { CredentialType, IdentifierType } from '../../types' +import IssuerController from '../IssuerController' +import { createApiIssuerRequest } from './apiTestData' +import { registerMockServicesByInterface, setupRabbitMQ, setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('IssuerController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let tenantId: string + + beforeAll(async () => { + await setupRabbitMQ() + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + await registerMockServicesByInterface(database) + + useContainer(Container) + + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(IssuerRepository) + Container.get(IssuerService) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + // Create Express server using routing-controllers + app = createExpressServer({ + controllers: [IssuerController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + //await testDbContainer.stop() + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete an issuer', async () => { + // Create prerequisites using test utilities + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + const issuerRequest = createApiIssuerRequest(asset.id, tenantId, [credentialDefinition.id], [credentialSchema.id]) + // Add identifierType and identifier that aren't in the utility + issuerRequest.identifierType = 'DID' + issuerRequest.identifier = 'did:test:456' + + const createResponse = await request.post('/roles/issuers').send(issuerRequest).expect(201) + + const created = createResponse.body.issuer + expect(created).toHaveProperty('id') + expect(created.name).toEqual('Test Issuer') + expect(created.description).toEqual('Test Issuer Description') + expect(created.type).toEqual('ARIES') + + // Retrieve all issuers + const getAllResponse = await request.get('/roles/issuers').expect(200) + expect(getAllResponse.body.issuers).toBeInstanceOf(Array) + expect(getAllResponse.body.issuers.length).toBeGreaterThan(0) + + // Retrieve the created issuer + const getResponse = await request.get(`/roles/issuers/${created.id}`).expect(200) + expect(getResponse.body.issuer.name).toEqual('Test Issuer') + expect(getResponse.body.issuer.organization).toEqual('Test Organization') + + // Update the issuer + const updateResponse = await request + .put(`/roles/issuers/${created.id}`) + .send({ + name: 'Updated Issuer', + description: 'Updated Description', + type: 'ARIES', + organization: 'Updated Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + credentialSchemas: [credentialSchema.id], + tenantId, + } satisfies IssuerRequest) + .expect(200) + + expect(updateResponse.body.issuer.name).toEqual('Updated Issuer') + expect(updateResponse.body.issuer.description).toEqual('Updated Description') + expect(updateResponse.body.issuer.organization).toEqual('Updated Organization') + + // Delete the issuer + await request.delete(`/roles/issuers/${created.id}`).expect(204) + + // Verify deletion (should return 404) + await request.get(`/roles/issuers/${created.id}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentId = '00000000-0000-0000-0000-000000000000' + + // Try to get a non-existent issuer + await request.get(`/roles/issuers/${nonExistentId}`).expect(404) + + // Try to update a non-existent issuer + const updateRequest = createApiIssuerRequest('', tenantId, [], []) + + await request.put(`/roles/issuers/${nonExistentId}`).send(updateRequest).expect(404) + + // Try to delete a non-existent issuer + await request.delete(`/roles/issuers/${nonExistentId}`).expect(404) + }) + + it('should validate request data when creating a issuer', async () => { + // Attempt to create an issuer with missing required fields + const invalidIssuerRequest = { + // Missing name, description, etc. + } + + await request.post('/roles/issuers').send(invalidIssuerRequest).expect(400) + + // Attempt to create an issuer with a non-existent credential definition + const nonExistentId = '00000000-0000-0000-0000-000000000000' + const invalidIssuerRequest2 = createApiIssuerRequest('', tenantId, [nonExistentId], [nonExistentId]) + + await request.post('/roles/issuers').send(invalidIssuerRequest2).expect(404) + }) + + it('should handle creating a issuer with multiple credential definitions', async () => { + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + + const credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + const credentialDefinition1 = await credentialDefinitionRepository.create({ + name: 'Test Definition 1', + tenantId: tenantId, + version: '1.0', + identifierType: IdentifierType.DID, + identifier: 'did:test:123', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + }) + + const credentialDefinition2 = await credentialDefinitionRepository.create({ + name: 'Test Definition 2', + tenantId: tenantId, + version: '1.0', + identifierType: IdentifierType.DID, + identifier: 'did:test:456', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + }) + + const issuerRequest = createApiIssuerRequest( + asset.id, + tenantId, + [credentialDefinition1.id, credentialDefinition2.id], + [credentialSchema.id], + ) + const createResponse = await request.post('/roles/issuers').send(issuerRequest).expect(201) + + const createdIssuer = createResponse.body.issuer + expect(createdIssuer).toHaveProperty('id') + expect(createdIssuer.name).toEqual('Test Issuer') + expect(createdIssuer.credentialDefinitions.length).toBe(2) + + // Verify that both credential definitions are included + const credentialDefIds = createdIssuer.credentialDefinitions.map((def: { id: string }) => def.id) + expect(credentialDefIds).toContain(credentialDefinition1.id) + expect(credentialDefIds).toContain(credentialDefinition2.id) + + // Clean up by deleting the issuer + await request.delete(`/roles/issuers/${createdIssuer.id}`).expect(204) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/MockSessionService.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/MockSessionService.ts new file mode 100644 index 000000000..69887d350 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/MockSessionService.ts @@ -0,0 +1,133 @@ +import { Service } from 'typedi' + +import { Tenant, TenantType, User } from '../../types' +import { Claims } from '../../types/auth/claims' +import { OidcSession } from '../../types/auth/session' +import { Token } from '../../types/auth/token' +import type { ISessionService } from '../../types/services/session' + +@Service() +export class MockSessionService implements ISessionService { + private user: User | null = null + private tenant: Tenant | null = null + private urlTenantId: string | null = null + private apiBaseUrl: string = 'http://localhost:3000' + private bearerToken: Token | undefined + private activeClaims: Claims | undefined + + public getCurrentUser(): User | null { + console.log('[MockSession] getCurrentUser called') + return ( + this.user ?? + ({ + id: 'user-id', + userName: 'user-name', + issuer: null, + clientId: null, + createdAt: new Date(), + updatedAt: new Date(), + } satisfies User | null) + ) + } + + public getCurrentTenant(): Tenant | null { + console.log('[MockSession] getCurrentTenant called') + return ( + this.tenant ?? + ({ + id: 'test-tenant', + createdAt: new Date(), + updatedAt: new Date(), + tenantType: TenantType.ROOT, + oidcIssuer: '', + tractionTenantId: null, + tractionApiUrl: null, + tractionWalletId: null, + tractionApiKey: null, + nonceBase64: null, + deletedAt: null, + } satisfies Tenant) + ) + } + + public getUrlTenantId(): string | null { + // If we have a urlTenantId set explicitly, return it + if (this.urlTenantId) { + return this.urlTenantId + } + + // Otherwise, fall back to the tenant's ID if we have a tenant + return this.tenant?.id || null + } + + public getBearerToken(): Token | undefined { + return this.bearerToken + } + + public getApiBaseUrl(): string | undefined { + return this.apiBaseUrl + } + + public getActiveClaims(): Claims | undefined { + return this.activeClaims + } + + public getCachedSessionByTokenHash(tokenHash: string): OidcSession | undefined { + return undefined // Not needed for tests + } + + // --- Test Control Methods --- + + /** Sets the user that will be returned by the next call to getCurrentUser */ + public setCurrentUser(user: User | null): void { + this.user = user + } + + /** Sets the tenant that will be returned by the next call to getCurrentTenant */ + public setCurrentTenant(tenant: Tenant | null): void { + this.tenant = tenant + + // Also set the urlTenantId when setting the tenant to ensure getTenantId() works + if (tenant) { + this.urlTenantId = tenant.id + } + } + + /** Sets the URL tenant ID explicitly */ + public setUrlTenantId(tenantId: string | null): void { + this.urlTenantId = tenantId + } + + /** Sets the API base URL */ + public setApiBaseUrl(url: string): void { + this.apiBaseUrl = url + } + + /** Sets the bearer token */ + public setBearerToken(token: Token | undefined): void { + this.bearerToken = token + } + + /** Sets the active claims */ + public setActiveClaims(claims: Claims | undefined): void { + this.activeClaims = claims + } + + /** Set request details similar to RequestContextMiddleware */ + public setRequestDetails(apiBaseUrl: string, tenantId: string | null, token?: Token): void { + this.apiBaseUrl = apiBaseUrl + this.urlTenantId = tenantId + this.bearerToken = token + } + + /** Resets the mock state */ + public clear(): void { + this.user = null + this.tenant = null + this.urlTenantId = null + this.apiBaseUrl = 'http://localhost:3000' + this.bearerToken = undefined + this.activeClaims = undefined + console.log('[MockSession] Cleared') + } +} diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/PersonaController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/PersonaController.integration.test.ts new file mode 100644 index 000000000..568e3a315 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/PersonaController.integration.test.ts @@ -0,0 +1,104 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { PersonaRequest } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { createMockDatabaseService, createTestAsset } from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import PersonaRepository from '../../database/repositories/PersonaRepository' +import DatabaseService from '../../services/DatabaseService' +import PersonaService from '../../services/PersonaService' +import PersonaController from '../PersonaController' +import { setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('PersonaController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + + beforeAll(async () => { + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + const mockDatabaseService = await createMockDatabaseService(database) + Container.set(DatabaseService, mockDatabaseService) + useContainer(Container) + Container.get(AssetRepository) + Container.get(PersonaRepository) + Container.get(PersonaService) + app = createExpressServer({ + controllers: [PersonaController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a persona', async () => { + // Create asset for headshotImage and bodyImage + const asset = await createTestAsset() + + // Create a persona + const createResponse = await request + .post('/personas') + .send({ + name: 'Test Persona', + role: 'Test Role', + description: 'Test Persona Description', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } satisfies PersonaRequest) + .expect(201) + + const created = createResponse.body.persona + expect(created).toHaveProperty('id') + expect(created.name).toEqual('Test Persona') + expect(created.role).toEqual('Test Role') + expect(created.description).toEqual('Test Persona Description') + expect(created.hidden).toEqual(false) + + // Retrieve all personas + const getAllResponse = await request.get('/personas').expect(200) + expect(Array.isArray(getAllResponse.body.personas)).toBeTruthy() + expect(getAllResponse.body.personas.length).toBeGreaterThan(0) + + // Retrieve the created persona by id + const getResponse = await request.get(`/personas/${created.slug}`).expect(200) + expect(getResponse.body.persona.name).toEqual('Test Persona') + expect(getResponse.body.persona.role).toEqual('Test Role') + + // Update the persona + const updateResponse = await request + .put(`/personas/${created.slug}`) + .send({ + name: 'Updated Persona', + role: 'Updated Role', + description: 'Updated Persona Description', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: true, + } satisfies PersonaRequest) + .expect(200) + const updatedPersona = updateResponse.body.persona + + expect(updateResponse.body.persona.name).toEqual('Updated Persona') + expect(updateResponse.body.persona.role).toEqual('Updated Role') + expect(updateResponse.body.persona.description).toEqual('Updated Persona Description') + expect(updateResponse.body.persona.hidden).toEqual(true) + + // Delete the persona + await request.delete(`/personas/${updatedPersona.slug}`).expect(204) + + // Verify deletion (should return 404) + await request.get(`/personas/${updatedPersona.slug}`).expect(404) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/PresentationScenarioController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/PresentationScenarioController.integration.test.ts new file mode 100644 index 000000000..ebca74532 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/PresentationScenarioController.integration.test.ts @@ -0,0 +1,372 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { StepAction } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createMockDatabaseService, + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestPersona, + createTestTenant, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import RelyingPartyRepository from '../../database/repositories/RelyingPartyRepository' +import ScenarioRepository from '../../database/repositories/ScenarioRepository' +import DatabaseService from '../../services/DatabaseService' +import ScenarioService from '../../services/ScenarioService' +import { RelyingPartyType, ScenarioType, StepActionType } from '../../types' +import PresentationScenarioController from '../PresentationScenarioController' +import { + createApiAcceptAction, + createApiButtonAction, + createApiIssuanceScenarioRequest, + createApiPresentationScenarioRequest, + createApiSetupConnectionAction, + createApiStepRequest, +} from './apiTestData' +import { setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('PresentationScenarioController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let tenantId: string + + beforeAll(async () => { + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + const mockDatabaseService = await createMockDatabaseService(database) + Container.set(DatabaseService, mockDatabaseService) + useContainer(Container) + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(RelyingPartyRepository) + Container.get(ScenarioRepository) + Container.get(ScenarioService) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + app = createExpressServer({ + controllers: [PresentationScenarioController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a presentation scenario with steps and actions', async () => { + // Create prerequisites using test utilities + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + // Create relying party + const relyingPartyRepository = Container.get(RelyingPartyRepository) + const relyingParty = await relyingPartyRepository.create({ + name: 'Test Relying Party', + type: RelyingPartyType.ARIES, + tenantId, + credentialDefinitions: [credentialDefinition.id], + description: 'Test relying party description', + organization: 'Test Organization', + logo: asset.id, + }) + + // Create persona + const persona = await createTestPersona(asset) + + const scenarioRequest = createApiPresentationScenarioRequest( + relyingParty.id, + persona.id, + asset.id, + credentialDefinition.id, + ) + + const createResponse = await request.post('/scenarios/presentations').send(scenarioRequest).expect(201) + + const createdScenario = createResponse.body.presentationScenario + expect(createdScenario).toHaveProperty('id') + expect(createdScenario.name).toEqual('Test Presentation Scenario') + expect(createdScenario.type).toEqual(ScenarioType.PRESENTATION) + expect(createdScenario.relyingParty.id).toEqual(relyingParty.id) + + // 3. Retrieve all presentation scenarios + const getAllResponse = await request.get('/scenarios/presentations').expect(200) + expect(getAllResponse.body.presentationScenarios).toBeInstanceOf(Array) + expect(getAllResponse.body.presentationScenarios.length).toBe(1) + + // 4. Retrieve the created scenario + const getResponse = await request.get(`/scenarios/presentations/${createdScenario.slug}`).expect(200) + expect(getResponse.body.presentationScenario.name).toEqual('Test Presentation Scenario') + + // 5. Update the scenario + const updateResponse = await request + .put(`/scenarios/presentations/${createdScenario.slug}`) + .send({ + ...scenarioRequest, + name: 'Updated Presentation Scenario Name', + }) + .expect(200) + expect(updateResponse.body.presentationScenario.name).toEqual('Updated Presentation Scenario Name') + const updatedScenario = updateResponse.body.presentationScenario + + // 6. Create a step for the scenario + const stepRequest = createApiStepRequest(asset.id, credentialDefinition.id, 2) + const createStepResponse = await request + .post(`/scenarios/presentations/${updatedScenario.slug}/steps`) + .send(stepRequest) + .expect(201) + + const createdStep = createStepResponse.body.step + expect(createdStep).toHaveProperty('id') + expect(createdStep.title).toEqual('Test Step') + + // 7. Retrieve all steps for the scenario + const getAllStepsResponse = await request.get(`/scenarios/presentations/${updatedScenario.slug}/steps`).expect(200) + expect(getAllStepsResponse.body.steps).toBeInstanceOf(Array) + expect(getAllStepsResponse.body.steps.length).toBe(2) + + // 8. Retrieve the created step + const getStepResponse = await request + .get(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}`) + .expect(200) + expect(getStepResponse.body.step.title).toEqual('Test Step') + + // 9. Update the step + const updateStepResponse = await request + .put(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}`) + .send({ + ...stepRequest, + title: 'Updated Test Step', + }) + .expect(200) + expect(updateStepResponse.body.step.title).toEqual('Updated Test Step') + + // 10. Create an additional action for the step + const actionRequest = createApiAcceptAction(credentialDefinition.id) + actionRequest.title = 'Additional Action' + actionRequest.text = 'Additional action text' + + const createActionResponse = await request + .post(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}/actions`) + .send(actionRequest) + .expect(201) + + const createdAction = createActionResponse.body.action + expect(createdAction).toHaveProperty('id') + expect(createdAction.title).toEqual('Additional Action') + expect(createdAction.actionType).toEqual(StepActionType.ACCEPT_CREDENTIAL) + + // 11. Retrieve all actions for the step + const getAllActionsResponse = await request + .get(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}/actions`) + .expect(200) + + expect(getAllActionsResponse.body.actions).toBeInstanceOf(Array) + expect(getAllActionsResponse.body.actions.length).toBe(2) // Original action from step creation plus the new one + + // 12. Retrieve the created action + const getActionResponse = await request + .get(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}/actions/${createdAction.id}`) + .expect(200) + + expect(getActionResponse.body.action.title).toEqual('Additional Action') + + // 13. Update the action + const updateActionResponse = await request + .put(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}/actions/${createdAction.id}`) + .send({ + ...actionRequest, + title: 'Updated Action Title', + }) + .expect(200) + + expect(updateActionResponse.body.action.title).toEqual('Updated Action Title') + + // 14. Delete the action + await request + .delete(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}/actions/${createdAction.id}`) + .expect(204) + + // 15. Delete the step + await request.delete(`/scenarios/presentations/${updatedScenario.slug}/steps/${createdStep.id}`).expect(204) + + // 16. Delete the scenario + await request.delete(`/scenarios/presentations/${updatedScenario.slug}`).expect(204) + + // 17. Verify scenario deletion + await request.get(`/scenarios/presentations/${updatedScenario.slug}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentSlug = '00000000-0000-0000-0000-000000000000' + + // Try to get a non-existent scenario + await request.get(`/scenarios/presentations/${nonExistentSlug}`).expect(404) + + // Try to get steps for a non-existent scenario + await request.get(`/scenarios/presentations/${nonExistentSlug}/steps`).expect(404) + + // Try to create a step for a non-existent scenario + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + const stepRequest = createApiStepRequest(asset.id, credentialDefinition.id) + await request.post(`/scenarios/presentations/${nonExistentSlug}/steps`).send(stepRequest).expect(404) + }) + + it('should validate request data when creating an issuance scenario', async () => { + // Attempt to create a scenario with missing required fields + const invalidScenarioRequest = { + // Missing name, description, etc. + } + + await request.post('/scenarios/presentations').send(invalidScenarioRequest).expect(400) + + // Set up assets and personas for testing + const asset = await createTestAsset() + const persona = await createTestPersona(asset) + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + // Attempt to create a scenario with a non-existent issuer + const nonExistentId = '00000000-0000-0000-0000-000000000000' + const invalidScenarioRequest2 = createApiIssuanceScenarioRequest( + nonExistentId, + persona.id, + asset.id, + credentialDefinition.id, + ) + + await request.post('/scenarios/issuances').send(invalidScenarioRequest2).expect(404) + }) + + it('should create, update, and verify different step action types', async () => { + // Set up assets and dependencies + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + const relyingPartyRepository = Container.get(RelyingPartyRepository) + const relyingParty = await relyingPartyRepository.create({ + name: 'Action Types Test RP', + type: RelyingPartyType.ARIES, + tenantId, + credentialDefinitions: [credentialDefinition.id], + description: 'Test RP for action types', + organization: 'Test Organization', + logo: asset.id, + }) + + const persona = await createTestPersona(asset) + + // Create a scenario with initial step + const scenarioRequest = createApiPresentationScenarioRequest( + relyingParty.id, + persona.id, + asset.id, + credentialDefinition.id, + ) + scenarioRequest.name = 'Action Types Test Scenario' + scenarioRequest.description = 'Testing different action types' + + const createResponse = await request.post('/scenarios/presentations').send(scenarioRequest).expect(201) + const createdScenario = createResponse.body.presentationScenario + expect(createdScenario).toHaveProperty('id') + + // Test ButtonAction + const buttonActionRequest = createApiButtonAction() + + const buttonActionResponse = await request + .post(`/scenarios/presentations/${createdScenario.slug}/steps/${createdScenario.steps[0].id}/actions`) + .send(buttonActionRequest) + .expect(201) + + const buttonAction = buttonActionResponse.body.action + expect(buttonAction).toHaveProperty('id') + expect(buttonAction.actionType).toEqual(StepActionType.BUTTON) + expect(buttonAction.goToStep).toEqual('step2') + + // Test SetupConnectionAction + const setupConnectionActionRequest = createApiSetupConnectionAction() + + const setupConnectionResponse = await request + .post(`/scenarios/presentations/${createdScenario.slug}/steps/${createdScenario.steps[0].id}/actions`) + .send(setupConnectionActionRequest) + .expect(201) + + const setupConnectionAction = setupConnectionResponse.body.action + expect(setupConnectionAction).toHaveProperty('id') + expect(setupConnectionAction.actionType).toEqual(StepActionType.SETUP_CONNECTION) + + // Test ChooseWalletAction + const chooseWalletActionRequest = { + title: 'Choose Wallet', + actionType: StepActionType.CHOOSE_WALLET, + text: 'Select your wallet', + } + + const chooseWalletResponse = await request + .post(`/scenarios/presentations/${createdScenario.slug}/steps/${createdScenario.steps[0].id}/actions`) + .send(chooseWalletActionRequest) + .expect(201) + + const chooseWalletAction = chooseWalletResponse.body.action + expect(chooseWalletAction).toHaveProperty('id') + expect(chooseWalletAction.actionType).toEqual(StepActionType.CHOOSE_WALLET) + + // Verify we can get all actions + const getAllActionsResponse = await request + .get(`/scenarios/presentations/${createdScenario.slug}/steps/${createdScenario.steps[0].id}/actions`) + .expect(200) + + const actions = getAllActionsResponse.body.actions + expect(actions).toBeInstanceOf(Array) + expect(actions.length).toBe(4) // Original + 3 new ones + + // Check each action type is present + const actionTypes = actions.map((a: StepAction) => a.actionType) + expect(actionTypes).toContain(StepActionType.ACCEPT_CREDENTIAL) + expect(actionTypes).toContain(StepActionType.BUTTON) + expect(actionTypes).toContain(StepActionType.SETUP_CONNECTION) + expect(actionTypes).toContain(StepActionType.CHOOSE_WALLET) + + // Test updating a specific action - modify the ButtonAction + const updateButtonRequest = { + ...buttonActionRequest, + text: 'Updated button text', + goToStep: 'newStep', + } + + const updateResponse = await request + .put( + `/scenarios/presentations/${createdScenario.slug}/steps/${createdScenario.steps[0].id}/actions/${buttonAction.id}`, + ) + .send(updateButtonRequest) + .expect(200) + + const updatedButtonAction = updateResponse.body.action + expect(updatedButtonAction.text).toEqual('Updated button text') + expect(updatedButtonAction.goToStep).toEqual('newStep') + + // Clean up by deleting the scenario + await request.delete(`/scenarios/presentations/${createdScenario.slug}`).expect(204) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/RelyingPartyController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/RelyingPartyController.integration.test.ts new file mode 100644 index 000000000..e72b76149 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/RelyingPartyController.integration.test.ts @@ -0,0 +1,209 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { RelyingPartyRequest } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createMockDatabaseService, + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestTenant, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import RelyingPartyRepository from '../../database/repositories/RelyingPartyRepository' +import DatabaseService from '../../services/DatabaseService' +import RelyingPartyService from '../../services/RelyingPartyService' +import { CredentialType, IdentifierType, RelyingPartyType } from '../../types' +import RelyingPartyController from '../RelyingPartyController' +import { setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') + +describe('RelyingPartyController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let tenantId: string + + beforeAll(async () => { + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + const mockDatabaseService = await createMockDatabaseService(database) + Container.set(DatabaseService, mockDatabaseService) + useContainer(Container) + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(RelyingPartyRepository) + Container.get(RelyingPartyService) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + app = createExpressServer({ + controllers: [RelyingPartyController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a relying party', async () => { + // Create prerequisites using test utilities + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + + // 1. Create a relying party + const relyingPartyRequest: RelyingPartyRequest = { + name: 'Test Relying Party', + description: 'Test relying party description', + type: RelyingPartyType.ARIES, + tenantId, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + } + + const createResponse = await request.post('/roles/relying-parties').send(relyingPartyRequest).expect(201) + + const createdRelyingParty = createResponse.body.relyingParty + expect(createdRelyingParty).toHaveProperty('id') + expect(createdRelyingParty.name).toEqual('Test Relying Party') + expect(createdRelyingParty.type).toEqual(RelyingPartyType.ARIES) + expect(createdRelyingParty.credentialDefinitions.length).toBe(1) + expect(createdRelyingParty.credentialDefinitions[0].id).toEqual(credentialDefinition.id) + + // 2. Retrieve all relying parties + const getAllResponse = await request.get('/roles/relying-parties').expect(200) + expect(getAllResponse.body.relyingParties).toBeInstanceOf(Array) + expect(getAllResponse.body.relyingParties.length).toBe(1) + + // 3. Retrieve the created relying party + const getResponse = await request.get(`/roles/relying-parties/${createdRelyingParty.id}`).expect(200) + expect(getResponse.body.relyingParty.name).toEqual('Test Relying Party') + + // 4. Update the relying party + const updateResponse = await request + .put(`/roles/relying-parties/${createdRelyingParty.id}`) + .send({ + ...relyingPartyRequest, + name: 'Updated Relying Party Name', + }) + .expect(200) + + expect(updateResponse.body.relyingParty.name).toEqual('Updated Relying Party Name') + + // 5. Delete the relying party + await request.delete(`/roles/relying-parties/${createdRelyingParty.id}`).expect(204) + + // 6. Verify relying party deletion + await request.get(`/roles/relying-parties/${createdRelyingParty.id}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentId = '00000000-0000-0000-0000-000000000000' + + // Try to get a non-existent relying party + await request.get(`/roles/relying-parties/${nonExistentId}`).expect(404) + + // Try to update a non-existent relying party + const updateRequest: RelyingPartyRequest = { + name: 'Non-existent Relying Party', + description: 'This relying party does not exist', + type: RelyingPartyType.ARIES, + tenantId, + credentialDefinitions: [], + } + + await request.put(`/roles/relying-parties/${nonExistentId}`).send(updateRequest).expect(404) + + // Try to delete a non-existent relying party + await request.delete(`/roles/relying-parties/${nonExistentId}`).expect(404) + }) + + it('should validate request data when creating a relying party', async () => { + // Attempt to create a relying party with missing required fields + const invalidRelyingPartyRequest = { + // Missing name, description, etc. + } + + await request.post('/roles/relying-parties').send(invalidRelyingPartyRequest).expect(400) + + // Attempt to create a relying party with a non-existent credential definition + const nonExistentId = '00000000-0000-0000-0000-000000000000' + const invalidRelyingPartyRequest2: RelyingPartyRequest = { + name: 'Invalid Relying Party', + description: 'Test description', + type: RelyingPartyType.ARIES, + tenantId, + credentialDefinitions: [nonExistentId], + } + + await request.post('/roles/relying-parties').send(invalidRelyingPartyRequest2).expect(404) + }) + + it('should handle creating a relying party with multiple credential definitions', async () => { + const asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + + const credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + const credentialDefinition1 = await credentialDefinitionRepository.create({ + name: 'Test Definition 1', + tenantId: tenantId, + version: '1.0', + identifierType: IdentifierType.DID, + identifier: 'did:test:123', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + }) + + const credentialDefinition2 = await credentialDefinitionRepository.create({ + name: 'Test Definition 2', + tenantId: tenantId, + version: '1.0', + identifierType: IdentifierType.DID, + identifier: 'did:test:456', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + }) + + const relyingPartyRequest: RelyingPartyRequest = { + name: 'Multi-Cred Relying Party', + description: 'Relying party with multiple credential definitions', + type: RelyingPartyType.ARIES, + tenantId, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + } + + const createResponse = await request.post('/roles/relying-parties').send(relyingPartyRequest).expect(201) + + const createdRelyingParty = createResponse.body.relyingParty + expect(createdRelyingParty).toHaveProperty('id') + expect(createdRelyingParty.name).toEqual('Multi-Cred Relying Party') + expect(createdRelyingParty.credentialDefinitions.length).toBe(2) + + // Verify that both credential definitions are included + const credentialDefIds = createdRelyingParty.credentialDefinitions.map((def: { id: string }) => def.id) + expect(credentialDefIds).toContain(credentialDefinition1.id) + expect(credentialDefIds).toContain(credentialDefinition2.id) + + // Clean up by deleting the relying party + await request.delete(`/roles/relying-parties/${createdRelyingParty.id}`).expect(204) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/ShowcaseController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/ShowcaseController.integration.test.ts new file mode 100644 index 000000000..f806fe25c --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/ShowcaseController.integration.test.ts @@ -0,0 +1,209 @@ +import './setup-env' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { ShowcaseRequest } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestIssuer, + createTestPersona, + createTestTenant, + createTestUser, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../database/repositories/CredentialDefinitionRepository' +import CredentialSchemaRepository from '../../database/repositories/CredentialSchemaRepository' +import IssuerRepository from '../../database/repositories/IssuerRepository' +import PersonaRepository from '../../database/repositories/PersonaRepository' +import ScenarioRepository from '../../database/repositories/ScenarioRepository' +import ShowcaseRepository from '../../database/repositories/ShowcaseRepository' +import TenantRepository from '../../database/repositories/TenantRepository' +import ShowcaseService from '../../services/ShowcaseService' +import TenantService from '../../services/TenantService' +import { ShowcaseStatus } from '../../types' +import ShowcaseController from '../ShowcaseController' +import { createApiFullTestData, createApiTestScenario } from './apiTestData' +import { registerMockServicesByInterface, setupRabbitMQ, setupTestDatabase } from './globalTestSetup' +import supertest = require('supertest') +import { MockSessionService } from './MockSessionService' + +describe('ShowcaseController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let tenantId: string + let sessionService: MockSessionService + + beforeAll(async () => { + await setupRabbitMQ() + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + await registerMockServicesByInterface(database) + useContainer(Container) + Container.get(TenantRepository) + sessionService = Container.get(MockSessionService) + Container.get(AssetRepository) + Container.get(CredentialSchemaRepository) + Container.get(CredentialDefinitionRepository) + Container.get(IssuerRepository) + Container.get(PersonaRepository) + Container.get(ScenarioRepository) + Container.get(ShowcaseRepository) + Container.get(TenantRepository) + Container.get(ShowcaseService) + Container.get(TenantService) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + sessionService.setCurrentTenant(tenant) + sessionService.setCurrentUser(await createTestUser('test-user')) + + app = createExpressServer({ + controllers: [ShowcaseController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a showcase with tenant association', async () => { + const testData = await createApiFullTestData(tenantId) + const createResponse = await request.post('/showcases').send(testData.showcaseRequest).expect(201) + + const createdShowcase = createResponse.body.showcase + expect(createdShowcase).toHaveProperty('id') + expect(createdShowcase.name).toEqual('Test Showcase') + expect(createdShowcase.tenantId).toEqual(tenantId) + expect(createdShowcase.status).toEqual(ShowcaseStatus.ACTIVE) + expect(createdShowcase.scenarios.length).toEqual(1) + expect(createdShowcase.personas.length).toEqual(1) + expect(createdShowcase.bannerImage).toBeDefined() + expect(createdShowcase.completionMessage).toEqual('Congratulations on completing the showcase!') + + // 2. Retrieve all showcases + const getAllResponse = await request.get('/showcases').expect(200) + expect(getAllResponse.body.showcases).toBeInstanceOf(Array) + expect(getAllResponse.body.showcases.length).toBe(1) + expect(getAllResponse.body.showcases[0].tenantId).toEqual(tenantId) + + // 3. Retrieve the created showcase + const getResponse = await request.get(`/showcases/${createdShowcase.slug}`).expect(200) + expect(getResponse.body.showcase.name).toEqual('Test Showcase') + expect(getResponse.body.showcase.tenantId).toEqual(tenantId) + + // 4. Update the showcase + const updatedRequest = { + ...testData.showcaseRequest, + name: 'Updated Showcase Name', + description: 'Updated showcase description', + status: ShowcaseStatus.PENDING, + } + + const updateResponse = await request.put(`/showcases/${createdShowcase.slug}`).send(updatedRequest).expect(200) + const updatedShowcase = updateResponse.body.showcase + + expect(updateResponse.body.showcase.name).toEqual('Updated Showcase Name') + expect(updateResponse.body.showcase.description).toEqual('Updated showcase description') + expect(updateResponse.body.showcase.status).toEqual(ShowcaseStatus.PENDING) + expect(updateResponse.body.showcase.tenantId).toEqual(tenantId) + + // 5. Delete the showcase + await request.delete(`/showcases/${updatedShowcase.slug}`).expect(204) + + // 6. Verify showcase deletion + await request.get(`/showcases/${updatedShowcase.slug}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentSlug = '00000000-0000-0000-0000-000000000000' + + // Try to get a non-existent showcase + await request.get(`/showcases/${nonExistentSlug}`).expect(404) + }) + + it('should validate request data when creating a showcase', async () => { + // Attempt to create a showcase with missing required fields + const invalidShowcaseRequest = { + // Missing name, description, etc. + } + + await request.post('/showcases').send(invalidShowcaseRequest).expect(400) + + // Attempt to create a showcase with non-existent IDs + const nonExistentId = '00000000-0000-0000-0000-000000000000' + const invalidShowcaseRequest2: ShowcaseRequest = { + name: 'Invalid Showcase', + description: 'Test description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenantId, + scenarios: [nonExistentId], + personas: [nonExistentId], + } + + await request.post('/showcases').send(invalidShowcaseRequest2).expect(404) + }) + + it('should validate tenant association when creating a showcase', async () => { + const asset = await createTestAsset() + const persona = await createTestPersona(asset) + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + const issuer = await createTestIssuer(asset, credentialDefinition, credentialSchema, tenantId) + + const scenario = await createApiTestScenario(asset.id, persona.id, issuer.id, credentialDefinition.id, tenantId) + + // Attempt to create a showcase with non-existent tenant + const invalidTenantShowcaseRequest: ShowcaseRequest = { + name: 'Invalid Tenant Showcase', + description: 'Test description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + scenarios: [scenario.id], + personas: [persona.id], + bannerImage: asset.id, + tenantId: 'non-existent-tenant', + } + + await request.post('/showcases').send(invalidTenantShowcaseRequest).expect(403) + }) + + it('should duplicate a showcase', async () => { + const testData = await createApiFullTestData(tenantId) + const createResponse = await request.post('/showcases').send(testData.showcaseRequest).expect(201) + const createdShowcase = createResponse.body.showcase + + const duplicateResponse = await request + .post(`/showcases/${createdShowcase.slug}/duplicate`) + .send({ tenantId: tenantId }) + .expect(201) + + const duplicatedShowcase = duplicateResponse.body.showcase + + expect(duplicatedShowcase.id).not.toEqual(createdShowcase.id) + expect(duplicatedShowcase.name).toEqual(`${createdShowcase.name} (Copy)`) + expect(duplicatedShowcase.tenantId).toEqual(createdShowcase.tenantId) + expect(duplicatedShowcase.status).toEqual(createdShowcase.status) + expect(duplicatedShowcase.scenarios.length).toEqual(createdShowcase.scenarios.length) + expect(duplicatedShowcase.personas.length).toEqual(createdShowcase.personas.length) + expect(duplicatedShowcase.bannerImage).toEqual(createdShowcase.bannerImage) + expect(duplicatedShowcase.completionMessage).toEqual(createdShowcase.completionMessage) + + // Delete the duplicated showcase + await request.delete(`/showcases/${duplicatedShowcase.slug}`).expect(204) + + // Verify the showcase is deleted + await request.get(`/showcases/${duplicatedShowcase.slug}`).expect(404) + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/TenantController.integration.test.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/TenantController.integration.test.ts new file mode 100644 index 000000000..61c928414 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/TenantController.integration.test.ts @@ -0,0 +1,177 @@ +import './setup-env' +import './setup-mocks' +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { environment } from 'bc-wallet-adapter-client-api/dist/environment' +import { TenantRequest } from 'bc-wallet-openapi' +import { Application } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import { Container } from 'typedi' + +import { + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestIssuer, + createTestPersona, + createTestScenario, + createTestTenant, +} from '../../database/repositories/__tests__/dbTestData' +import ShowcaseRepository from '../../database/repositories/ShowcaseRepository' +import TenantRepository from '../../database/repositories/TenantRepository' +import ShowcaseService from '../../services/ShowcaseService' +import TenantService from '../../services/TenantService' +import { ShowcaseStatus } from '../../types' +import TenantController from '../TenantController' +import { registerMockServicesByInterface, setupRabbitMQ, setupTestDatabase } from './globalTestSetup' +import { MockSessionService } from './MockSessionService' +import supertest = require('supertest') + +describe('TenantController Integration Tests', () => { + let client: PGlite + let app: Application + let request: any + let sessionService: MockSessionService + + beforeAll(async () => { + process.env.ENCRYPTION_KEY = environment.encryption.ENCRYPTION_KEY = 'F5XH4zeMFB6nLKY7g15kpkVEcxFkGokGbAKSPbzaTEwe' + process.env.NONCE_SIZE = `${environment.encryption.NONCE_SIZE ?? 12}` + await setupRabbitMQ() + const { client: pgClient, database } = await setupTestDatabase() + client = pgClient + await registerMockServicesByInterface(database) + useContainer(Container) + Container.get(TenantRepository) + Container.get(TenantService) + sessionService = Container.get(MockSessionService) + app = createExpressServer({ + controllers: [TenantController], + authorizationChecker: () => true, + }) + request = supertest(app) + }) + + afterAll(async () => { + await client.close() + Container.reset() + }) + + it('should create, retrieve, update, and delete a tenant', async () => { + // 1. Create a tenant + const tenantRequest: TenantRequest = { + id: 'test-tenant-1', + tractionTenantId: 'a7d9b6bd-f263-4cf6-9b6d-cbc5f0e9f0c6', + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const createResponse = await request.post('/tenants').send(tenantRequest).expect(201) + + const createdTenant = createResponse.body.tenant + expect(createdTenant).toHaveProperty('id') + expect(createdTenant.id).toEqual('test-tenant-1') + expect(createdTenant.tractionTenantId).toEqual('a7d9b6bd-f263-4cf6-9b6d-cbc5f0e9f0c6') + expect(createdTenant.oidcIssuer).toEqual('https://auth-server/auth/realms/test') + expect(createdTenant.createdAt).toBeDefined() + + // Check that default issuer and relying party were created + expect(createdTenant.issuers).toHaveLength(1) + expect(createdTenant.issuers[0].name).toEqual('Default Issuer for test-tenant-1') + expect(createdTenant.relyingParties).toHaveLength(1) + expect(createdTenant.relyingParties[0].name).toEqual('Default Relying Party for test-tenant-1') + + // 2. Retrieve all tenants + const getAllResponse = await request.get('/tenants').expect(200) + expect(getAllResponse.body.tenants).toBeInstanceOf(Array) + expect(getAllResponse.body.tenants.length).toBe(1) + + // 4. Update the tenant + const updatedRequest: TenantRequest = { + id: createdTenant.id, + oidcIssuer: 'https://auth-server/auth/realms/updated-test', + tractionTenantId: 'b8e9c7ce-f374-4df7-8c7e-dcd6f1f0f1d7', + } + + const updateResponse = await request.put(`/tenants/${createdTenant.id}`).send(updatedRequest).expect(200) + const updatedTenant = updateResponse.body.tenant + + expect(updatedTenant.id).toEqual(createdTenant.id) + expect(updatedTenant.oidcIssuer).toEqual('https://auth-server/auth/realms/updated-test') + expect(updatedTenant.tractionTenantId).toEqual('b8e9c7ce-f374-4df7-8c7e-dcd6f1f0f1d7') + + // 5. Delete the tenant (use the original ID since it wasn't changed) + await request.delete(`/tenants/${createdTenant.id}`).expect(204) + + // 6. Verify tenant deletion + await request.get(`/tenants/${createdTenant.id}`).expect(404) + }) + + it('should handle errors when accessing non-existent resources', async () => { + const nonExistentId = 'non-existent-tenant' + + // Try to get a non-existent tenant + await request.get(`/tenants/${nonExistentId}`).expect(404) + }) + + it('should validate request data when creating a tenant', async () => { + // Attempt to create a tenant with missing required fields + const invalidTenantRequest = { + // Missing id + } + + await request.post('/tenants').send(invalidTenantRequest).expect(400) + }) + + it('should cascade delete showcases when a tenant is deleted', async () => { + // Register the required services + Container.get(ShowcaseRepository) + Container.get(ShowcaseService) + + // Create test tenant + const tenant = await createTestTenant('cascade-test-tenant') + sessionService.setCurrentTenant(tenant) + + // Create test data + const asset = await createTestAsset() + const persona = await createTestPersona(asset) + const schema = await createTestCredentialSchema() + const definition = await createTestCredentialDefinition(asset, schema, tenant.id) + const issuer = await createTestIssuer(asset, definition, schema, tenant.id) + const scenario = await createTestScenario(asset, persona, issuer, definition.id, tenant.id) + + // Create a showcase associated with the tenant + const showcaseService = Container.get(ShowcaseService) + const showcase = await showcaseService.createShowcase({ + name: 'Test Cascade Showcase', + description: 'Testing cascade delete', + status: ShowcaseStatus.ACTIVE, + hidden: false, + scenarios: [scenario.id], + personas: [persona.id], + credentialDefinitions: [definition.id], + bannerImage: asset.id, + tenantId: tenant.id, + }) + + // Verify showcase was created + const showcaseRepository = Container.get(ShowcaseRepository) + // Get the showcase ID first, then use it to verify showcase exists + const showcaseId = await showcaseRepository.findIdBySlug(showcase.slug, tenant.id) + const showcaseBeforeDelete = await showcaseRepository.findById(showcaseId) + expect(showcaseBeforeDelete).toBeDefined() + + // Delete the tenant + await request.delete(`/tenants/${tenant.id}`).expect(204) + + // Verify tenant was deleted + await request.get(`/tenants/${tenant.id}`).expect(404) + + // Verify showcase was also deleted (cascade) + try { + await showcaseRepository.findIdBySlug(showcase.slug, tenant.id) + // Should not reach here + fail('Showcase should have been deleted') + } catch (error) { + expect(error.message).toContain('No showcase found for slug: test-cascade-showcase') + } + }) +}) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/__mocks__/auth.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/__mocks__/auth.ts new file mode 100644 index 000000000..d136d766d --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/__mocks__/auth.ts @@ -0,0 +1,19 @@ +export function RootTenantAuthorized() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Just return the original method without modification + return descriptor + } +} + +export function SoftTenantAuthorized() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + // Just return the original method without modification + return descriptor + } +} + +export const checkRoles = jest.fn().mockReturnValue(true) +export const isAccessTokenValid = jest.fn().mockResolvedValue(true) +export const getBasePath = jest.fn((path) => `${path || ''}`) +export const authorizationChecker = jest.fn().mockResolvedValue(true) +export const isAccessTokenExpired = jest.fn().mockReturnValue(false) diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/apiTestData.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/apiTestData.ts new file mode 100644 index 000000000..f8944e67f --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/apiTestData.ts @@ -0,0 +1,177 @@ +import { + AcceptCredentialActionRequest, + ButtonActionRequest, + ChooseWalletActionRequest, + IssuanceScenarioRequest, + IssuerRequest, + PresentationScenarioRequest, + SetupConnectionActionRequest, + ShowcaseRequest, + ShowcaseStatus, + StepActionType, + StepRequest, + StepType, +} from 'bc-wallet-openapi' +import Container from 'typedi' + +import { + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestIssuer, + createTestPersona, + createTestScenario, +} from '../../database/repositories/__tests__/dbTestData' +import AssetRepository from '../../database/repositories/AssetRepository' +import IssuerRepository from '../../database/repositories/IssuerRepository' +import PersonaRepository from '../../database/repositories/PersonaRepository' + +// Step Actions +export function createApiAcceptAction(credentialDefinitionId: string): AcceptCredentialActionRequest { + return { + title: 'Test Action', + actionType: StepActionType.AcceptCredential, + text: 'Test action text', + credentialDefinitionId, + } +} + +export function createApiButtonAction(): ButtonActionRequest { + return { + title: 'Button Action', + actionType: StepActionType.Button, + text: 'Click this button', + goToStep: 'step2', + } +} + +export function createApiSetupConnectionAction(): SetupConnectionActionRequest { + return { + title: 'Setup Connection', + actionType: StepActionType.SetupConnection, + text: 'Set up a connection', + } +} + +export function createApiChooseWalletAction(): ChooseWalletActionRequest { + return { + title: 'Choose Wallet', + actionType: StepActionType.ChooseWallet, + text: 'Select your wallet', + } +} + +// Step Request +export function createApiStepRequest(assetId: string, credentialDefinitionId: string, order: number = 1): StepRequest { + return { + title: 'Test Step', + description: 'Test step description', + order, + type: StepType.HumanTask, + asset: assetId, + actions: [createApiAcceptAction(credentialDefinitionId)], + } +} + +// Scenario Requests +export function createApiPresentationScenarioRequest( + relyingPartyId: string, + personaId: string, + assetId: string, + credentialDefinitionId: string, +): PresentationScenarioRequest { + return { + name: 'Test Presentation Scenario', + description: 'Test scenario description', + steps: [createApiStepRequest(assetId, credentialDefinitionId)], + personas: [personaId], + relyingParty: relyingPartyId, + hidden: false, + } +} + +export function createApiIssuanceScenarioRequest( + issuerId: string, + personaId: string, + assetId: string, + credentialDefinitionId: string, +): IssuanceScenarioRequest { + return { + name: 'Test Issuance Scenario', + description: 'Test scenario description', + steps: [createApiStepRequest(assetId, credentialDefinitionId)], + personas: [personaId], + issuer: issuerId, + hidden: false, + } +} + +// Showcase Request +export function createApiShowcaseRequest( + scenarioId: string, + personaId: string, + assetId: string, + tenantId: string, +): ShowcaseRequest { + return { + name: 'Test Showcase', + description: 'Test showcase description', + status: ShowcaseStatus.Active, + hidden: false, + scenarios: [scenarioId], + personas: [personaId], + bannerImage: assetId, + completionMessage: 'Congratulations on completing the showcase!', + tenantId: tenantId, + } +} + +export function createApiIssuerRequest( + assetId: string, + tenantId: string, + credentialDefinitions: string[], + credentialSchemas: string[], +): IssuerRequest { + return { + name: 'Test Issuer', + description: 'Test Issuer Description', + type: 'ARIES', + tenantId, + organization: 'Test Organization', + logo: assetId, + credentialDefinitions: credentialDefinitions, + credentialSchemas: credentialSchemas, + } +} + +export async function createApiFullTestData(tenantId: string): Promise<{ showcaseRequest: ShowcaseRequest }> { + const asset = await createTestAsset() + const persona = await createTestPersona(asset) + const credentialSchema = await createTestCredentialSchema() + const credentialDefinition = await createTestCredentialDefinition(asset, credentialSchema, tenantId) + const issuer = await createTestIssuer(asset, credentialDefinition, credentialSchema, tenantId) + const scenario = await createTestScenario(asset, persona, issuer, credentialDefinition.id, tenantId) + const showcaseRequest = createApiShowcaseRequest(scenario.id, persona.id, asset.id, tenantId) + return { showcaseRequest } +} + +export async function createApiTestScenario( + assetId: string, + personaId: string, + issuerId: string, + credentialDefinitionId: string, + tenantId: string +): Promise<{ id: string } & IssuanceScenarioRequest> { + const assetRepository = Container.get(AssetRepository) + const personaRepository = Container.get(PersonaRepository) + const issuerRepository = Container.get(IssuerRepository) + + const asset = await assetRepository.findById(assetId) + const persona = await personaRepository.findById(personaId) + const issuer = await issuerRepository.findById(issuerId) + + const scenario = await createTestScenario(asset, persona, issuer, credentialDefinitionId, tenantId) + const scenarioRequest = createApiIssuanceScenarioRequest(issuerId, personaId, assetId, credentialDefinitionId) + + return { id: scenario.id, ...scenarioRequest } +} diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/globalTeardown.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/globalTeardown.ts new file mode 100644 index 000000000..b2037ee94 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/globalTeardown.ts @@ -0,0 +1,13 @@ +import { StartedRabbitMQContainer } from '@testcontainers/rabbitmq' + +declare global { + var rabbitMQContainer: StartedRabbitMQContainer | undefined +} + +export default async function () { + if (global.rabbitMQContainer) { + console.log('Stopping RabbitMQ') + await global.rabbitMQContainer.stop() + global.rabbitMQContainer = undefined + } +} diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/globalTestSetup.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/globalTestSetup.ts new file mode 100644 index 000000000..122a47c11 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/globalTestSetup.ts @@ -0,0 +1,45 @@ +import { PGlite } from '@electric-sql/pglite' +import { RabbitMQContainer, StartedRabbitMQContainer } from '@testcontainers/rabbitmq' +import { AdapterClientApi } from 'bc-wallet-adapter-client-api' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import Container from 'typedi' + +import { createMockDatabaseService } from '../../database/repositories/__tests__/dbTestData' +import * as schema from '../../database/schema' +import DatabaseService from '../../services/DatabaseService' +import { MockSessionService } from './MockSessionService' + +export async function registerMockServicesByInterface(database: NodePgDatabase) { + const oidcSessionService = Container.get(MockSessionService) + Container.set('ISessionService', oidcSessionService) + const adapterClientApi = Container.get(AdapterClientApi) + Container.set('IAdapterClientApi', adapterClientApi) + const mockDatabaseService = await createMockDatabaseService(database) + Container.set(DatabaseService, mockDatabaseService) +} + +declare global { + var rabbitMQContainer: StartedRabbitMQContainer | undefined +} + +export async function setupTestDatabase(): Promise<{ client: PGlite; database: NodePgDatabase }> { + const client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + return { client, database } +} + +export async function setupRabbitMQ() { + if (!global.rabbitMQContainer) { + global.rabbitMQContainer = await new RabbitMQContainer('rabbitmq:4').start() + + process.env.AMQ_HOST = global.rabbitMQContainer.getHost() + process.env.AMQ_PORT = `${global.rabbitMQContainer.getMappedPort(5672)}` + process.env.AMQ_TRANSPORT = 'tcp' + process.env.ENCRYPTION_KEY = 'F5XH4zeMFB6nLKY7g15kpkVEcxFkGokGbAKSPbzaTEwe' + } + + return global.rabbitMQContainer +} diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/setup-env.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/setup-env.ts new file mode 100644 index 000000000..c43e9f357 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/setup-env.ts @@ -0,0 +1,3 @@ +process.env.OIDC_ROOT_ISSUER_URL = 'https://auth-server/auth/realms/BC' +process.env.OIDC_ROOT_CLIENT_ID = 'client-id' +process.env.OIDC_ROOT_CLIENT_SECRET = 'client-secret' diff --git a/apps/bc-wallet-api-server/src/controllers/__tests__/setup-mocks.ts b/apps/bc-wallet-api-server/src/controllers/__tests__/setup-mocks.ts new file mode 100644 index 000000000..7474f2504 --- /dev/null +++ b/apps/bc-wallet-api-server/src/controllers/__tests__/setup-mocks.ts @@ -0,0 +1,5 @@ +jest.mock('../../utils/auth', () => ({ + ...jest.requireActual('../../utils/auth'), + RootTenantAuthorized: () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => descriptor, + SoftTenantAuthorized: () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => descriptor, +})) diff --git a/apps/bc-wallet-api-server/src/database/customTypes/pg.ts b/apps/bc-wallet-api-server/src/database/customTypes/pg.ts new file mode 100644 index 000000000..6ef9a4355 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/customTypes/pg.ts @@ -0,0 +1,20 @@ +import { customType } from 'drizzle-orm/pg-core' +import { Buffer } from 'buffer' + +export const customBytea = customType<{ + data: Buffer + default: false +}>({ + dataType() { + return 'bytea' + }, + fromDriver(value) { + if (value instanceof Uint8Array) { + return Buffer.from(value) + } + if (typeof value === 'string') { + return Buffer.from(value.replace(/^\\x/, ''), 'hex') + } + throw new Error('Invalid value for bytea column') + }, +}) diff --git a/apps/bc-wallet-api-server/src/database/migrations/0000_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0000_bc-wallet-api.sql new file mode 100644 index 000000000..c463b7fd2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0000_bc-wallet-api.sql @@ -0,0 +1,312 @@ +CREATE TYPE "public"."CredentialAttributeType" AS ENUM('STRING', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE');--> statement-breakpoint +CREATE TYPE "public"."CredentialType" AS ENUM('ANONCRED');--> statement-breakpoint +CREATE TYPE "public"."IdentifierType" AS ENUM('DID');--> statement-breakpoint +CREATE TYPE "public"."IssuerType" AS ENUM('ARIES');--> statement-breakpoint +CREATE TYPE "public"."StepType" AS ENUM('HUMAN_TASK', 'SERVICE', 'SCENARIO');--> statement-breakpoint +CREATE TYPE "public"."ScenarioType" AS ENUM('ISSUANCE', 'PRESENTATION');--> statement-breakpoint +CREATE TYPE "public"."ShowcaseStatus" AS ENUM('PENDING', 'ACTIVE', 'ARCHIVED');--> statement-breakpoint +CREATE TYPE "public"."RelyingPartyType" AS ENUM('ARIES');--> statement-breakpoint +CREATE TYPE "public"."Source" AS ENUM('IMPORTED', 'CREATED');--> statement-breakpoint +CREATE TABLE "ariesProofRequest" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "attributes" jsonb NOT NULL, + "predicates" jsonb NOT NULL, + "step_action" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "ariesProofRequest_step_action_unique" UNIQUE("step_action") +); +--> statement-breakpoint +CREATE TABLE "asset" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "media_type" text NOT NULL, + "file_name" text, + "description" text, + "content" "bytea" NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "credentialAttribute" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "value" text NOT NULL, + "type" "CredentialAttributeType" NOT NULL, + "credential_schema" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "credentialDefinition" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "version" text NOT NULL, + "identifier_type" "IdentifierType", + "identifier" text, + "credential_schema" uuid NOT NULL, + "icon" uuid, + "type" "CredentialType" NOT NULL, + "approved_by" uuid, + "approved_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "credentialRepresentation" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "credential_definition" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "credentialSchema" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "identifier_type" "IdentifierType", + "source" "Source" DEFAULT 'CREATED', + "identifier" text, + "name" text NOT NULL, + "version" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "relyingPartiesToCredentialDefinitions" ( + "relying_party" uuid NOT NULL, + "credential_definition" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk" PRIMARY KEY("relying_party","credential_definition") +); +--> statement-breakpoint +CREATE TABLE "relyingParty" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "type" "RelyingPartyType" NOT NULL, + "description" text NOT NULL, + "organization" text, + "logo" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "issuer" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "type" "IssuerType" NOT NULL, + "description" text NOT NULL, + "organization" text, + "logo" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "issuersToCredentialDefinitions" ( + "issuer" uuid NOT NULL, + "credential_definition" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "issuersToCredentialDefinitions_issuer_credential_definition_pk" PRIMARY KEY("issuer","credential_definition") +); +--> statement-breakpoint +CREATE TABLE "issuersToCredentialSchemas" ( + "issuer" uuid NOT NULL, + "credential_schema" uuid NOT NULL, + CONSTRAINT "issuersToCredentialSchemas_issuer_credential_schema_pk" PRIMARY KEY("issuer","credential_schema") +); +--> statement-breakpoint +CREATE TABLE "step" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "order" integer NOT NULL, + "type" "StepType" NOT NULL, + "sub_scenario" uuid, + "scenario" uuid NOT NULL, + "asset" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "step_order_scenario_unique" UNIQUE("order","scenario") +); +--> statement-breakpoint +CREATE TABLE "stepAction" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "action_type" text NOT NULL, + "title" text NOT NULL, + "text" text NOT NULL, + "step" uuid, + "credential_definition_id" text, + "connection_id" text, + "go_to_step" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "scenario" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "description" text NOT NULL, + "scenario_type" "ScenarioType" NOT NULL, + "issuer" uuid, + "hidden" boolean DEFAULT false NOT NULL, + "relying_party" uuid, + "banner_image" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "scenario_slug_unique" UNIQUE("slug"), + CONSTRAINT "scenario_type_check" CHECK ( + (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR + (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL) + ) +); +--> statement-breakpoint +CREATE TABLE "revocationInfo" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "credential_definition" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "revocationInfo_credential_definition_unique" UNIQUE("credential_definition") +); +--> statement-breakpoint +CREATE TABLE "persona" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "role" text NOT NULL, + "description" text NOT NULL, + "headshot_image" uuid, + "body_image" uuid, + "hidden" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "persona_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "scenariosToPersonas" ( + "scenario" uuid NOT NULL, + "persona" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "scenariosToPersonas_scenario_persona_pk" PRIMARY KEY("scenario","persona") +); +--> statement-breakpoint +CREATE TABLE "tenant" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "showcase" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" text NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "description" text NOT NULL, + "completionMessage" text, + "status" "ShowcaseStatus" NOT NULL, + "hidden" boolean DEFAULT false NOT NULL, + "banner_image" uuid, + "approved_by" uuid, + "approved_at" timestamp, + "created_by" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "showcase_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "showcasesToCredentialDefinitions" ( + "showcase" uuid NOT NULL, + "credential_definition" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "showcasesToCredentialDefinitions_showcase_credential_definition_pk" PRIMARY KEY("showcase","credential_definition") +); +--> statement-breakpoint +CREATE TABLE "showcasesToPersonas" ( + "showcase" uuid NOT NULL, + "persona" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "showcasesToPersonas_showcase_persona_pk" PRIMARY KEY("showcase","persona") +); +--> statement-breakpoint +CREATE TABLE "showcasesToScenarios" ( + "showcase" uuid NOT NULL, + "scenario" uuid, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "showcasesToScenarios_showcase_scenario_pk" PRIMARY KEY("showcase","scenario") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "userName" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ariesProofRequest" ADD CONSTRAINT "ariesProofRequest_step_action_stepAction_id_fk" FOREIGN KEY ("step_action") REFERENCES "public"."stepAction"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credentialAttribute" ADD CONSTRAINT "credentialAttribute_credential_schema_credentialSchema_id_fk" FOREIGN KEY ("credential_schema") REFERENCES "public"."credentialSchema"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credentialDefinition" ADD CONSTRAINT "credentialDefinition_credential_schema_credentialSchema_id_fk" FOREIGN KEY ("credential_schema") REFERENCES "public"."credentialSchema"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credentialDefinition" ADD CONSTRAINT "credentialDefinition_icon_asset_id_fk" FOREIGN KEY ("icon") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credentialDefinition" ADD CONSTRAINT "credentialDefinition_approved_by_user_id_fk" FOREIGN KEY ("approved_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credentialRepresentation" ADD CONSTRAINT "credentialRepresentation_credential_definition_credentialDefinition_id_fk" FOREIGN KEY ("credential_definition") REFERENCES "public"."credentialDefinition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relyingPartiesToCredentialDefinitions" ADD CONSTRAINT "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk" FOREIGN KEY ("relying_party") REFERENCES "public"."relyingParty"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relyingPartiesToCredentialDefinitions" ADD CONSTRAINT "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk" FOREIGN KEY ("credential_definition") REFERENCES "public"."credentialDefinition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "relyingParty" ADD CONSTRAINT "relyingParty_logo_asset_id_fk" FOREIGN KEY ("logo") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issuer" ADD CONSTRAINT "issuer_logo_asset_id_fk" FOREIGN KEY ("logo") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issuersToCredentialDefinitions" ADD CONSTRAINT "issuersToCredentialDefinitions_issuer_issuer_id_fk" FOREIGN KEY ("issuer") REFERENCES "public"."issuer"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issuersToCredentialDefinitions" ADD CONSTRAINT "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk" FOREIGN KEY ("credential_definition") REFERENCES "public"."credentialDefinition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issuersToCredentialSchemas" ADD CONSTRAINT "issuersToCredentialSchemas_issuer_issuer_id_fk" FOREIGN KEY ("issuer") REFERENCES "public"."issuer"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issuersToCredentialSchemas" ADD CONSTRAINT "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk" FOREIGN KEY ("credential_schema") REFERENCES "public"."credentialSchema"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "step" ADD CONSTRAINT "step_sub_scenario_scenario_id_fk" FOREIGN KEY ("sub_scenario") REFERENCES "public"."scenario"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "step" ADD CONSTRAINT "step_scenario_scenario_id_fk" FOREIGN KEY ("scenario") REFERENCES "public"."scenario"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "step" ADD CONSTRAINT "step_asset_asset_id_fk" FOREIGN KEY ("asset") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stepAction" ADD CONSTRAINT "stepAction_step_step_id_fk" FOREIGN KEY ("step") REFERENCES "public"."step"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenario" ADD CONSTRAINT "scenario_issuer_issuer_id_fk" FOREIGN KEY ("issuer") REFERENCES "public"."issuer"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenario" ADD CONSTRAINT "scenario_relying_party_relyingParty_id_fk" FOREIGN KEY ("relying_party") REFERENCES "public"."relyingParty"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenario" ADD CONSTRAINT "scenario_banner_image_asset_id_fk" FOREIGN KEY ("banner_image") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "revocationInfo" ADD CONSTRAINT "revocationInfo_credential_definition_credentialDefinition_id_fk" FOREIGN KEY ("credential_definition") REFERENCES "public"."credentialDefinition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "persona" ADD CONSTRAINT "persona_headshot_image_asset_id_fk" FOREIGN KEY ("headshot_image") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "persona" ADD CONSTRAINT "persona_body_image_asset_id_fk" FOREIGN KEY ("body_image") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenariosToPersonas" ADD CONSTRAINT "scenariosToPersonas_scenario_scenario_id_fk" FOREIGN KEY ("scenario") REFERENCES "public"."scenario"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenariosToPersonas" ADD CONSTRAINT "scenariosToPersonas_persona_persona_id_fk" FOREIGN KEY ("persona") REFERENCES "public"."persona"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcase" ADD CONSTRAINT "showcase_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcase" ADD CONSTRAINT "showcase_banner_image_asset_id_fk" FOREIGN KEY ("banner_image") REFERENCES "public"."asset"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcase" ADD CONSTRAINT "showcase_approved_by_user_id_fk" FOREIGN KEY ("approved_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcase" ADD CONSTRAINT "showcase_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcasesToCredentialDefinitions" ADD CONSTRAINT "showcasesToCredentialDefinitions_showcase_showcase_id_fk" FOREIGN KEY ("showcase") REFERENCES "public"."showcase"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcasesToCredentialDefinitions" ADD CONSTRAINT "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk" FOREIGN KEY ("credential_definition") REFERENCES "public"."credentialDefinition"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcasesToPersonas" ADD CONSTRAINT "showcasesToPersonas_showcase_showcase_id_fk" FOREIGN KEY ("showcase") REFERENCES "public"."showcase"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcasesToPersonas" ADD CONSTRAINT "showcasesToPersonas_persona_persona_id_fk" FOREIGN KEY ("persona") REFERENCES "public"."persona"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcasesToScenarios" ADD CONSTRAINT "showcasesToScenarios_showcase_showcase_id_fk" FOREIGN KEY ("showcase") REFERENCES "public"."showcase"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "showcasesToScenarios" ADD CONSTRAINT "showcasesToScenarios_scenario_scenario_id_fk" FOREIGN KEY ("scenario") REFERENCES "public"."scenario"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_credential_attributes" ON "credentialAttribute" USING btree ("credential_schema");--> statement-breakpoint +CREATE INDEX "idx_cd_icon" ON "credentialDefinition" USING btree ("icon");--> statement-breakpoint +CREATE INDEX "idx_cd_credentialSchema" ON "credentialDefinition" USING btree ("credential_schema");--> statement-breakpoint +CREATE INDEX "idx_cd_approvedBy" ON "credentialDefinition" USING btree ("approved_by");--> statement-breakpoint +CREATE INDEX "idx_credential_representations" ON "credentialRepresentation" USING btree ("credential_definition");--> statement-breakpoint +CREATE INDEX "idx_relyingParty" ON "relyingPartiesToCredentialDefinitions" USING btree ("relying_party");--> statement-breakpoint +CREATE INDEX "idx_logo" ON "relyingParty" USING btree ("logo");--> statement-breakpoint +CREATE INDEX "idx_issuer_logo" ON "issuer" USING btree ("logo");--> statement-breakpoint +CREATE INDEX "idx_issuer" ON "issuersToCredentialDefinitions" USING btree ("issuer");--> statement-breakpoint +CREATE INDEX "idx_issuer_schemas" ON "issuersToCredentialSchemas" USING btree ("issuer");--> statement-breakpoint +CREATE INDEX "idx_scenarios_steps" ON "step" USING btree ("scenario");--> statement-breakpoint +CREATE INDEX "idx_asset_steps" ON "step" USING btree ("asset");--> statement-breakpoint +CREATE INDEX "idx_step_actions" ON "stepAction" USING btree ("step");--> statement-breakpoint +CREATE INDEX "idx_relyingParty_issuer_scenario" ON "scenario" USING btree ("relying_party");--> statement-breakpoint +CREATE INDEX "idx_issuer_scenario" ON "scenario" USING btree ("issuer");--> statement-breakpoint +CREATE INDEX "idx_bannerImage_issuer_scenario" ON "scenario" USING btree ("banner_image");--> statement-breakpoint +CREATE INDEX "idx_revocation_info" ON "revocationInfo" USING btree ("credential_definition");--> statement-breakpoint +CREATE INDEX "idx_headshotImage" ON "persona" USING btree ("headshot_image");--> statement-breakpoint +CREATE INDEX "idx_bodyImage" ON "persona" USING btree ("body_image");--> statement-breakpoint +CREATE INDEX "idx_scenario" ON "scenariosToPersonas" USING btree ("scenario");--> statement-breakpoint +CREATE INDEX "idx_bannerImage" ON "showcase" USING btree ("banner_image");--> statement-breakpoint +CREATE INDEX "idx_tenant_id" ON "showcase" USING btree ("tenant_id");--> statement-breakpoint +CREATE INDEX "idx_sc_approvedBy" ON "showcase" USING btree ("approved_by");--> statement-breakpoint +CREATE INDEX "showcases_to_credential_definitions_showcase_idx" ON "showcasesToCredentialDefinitions" USING btree ("showcase");--> statement-breakpoint +CREATE INDEX "showcases_to_personas_showcase_idx" ON "showcasesToPersonas" USING btree ("showcase");--> statement-breakpoint +CREATE INDEX "showcases_to_scenarios_showcase_idx" ON "showcasesToScenarios" USING btree ("showcase"); \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/0001_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0001_bc-wallet-api.sql new file mode 100644 index 000000000..5dc41b8d2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0001_bc-wallet-api.sql @@ -0,0 +1,2 @@ +ALTER TABLE "user" ADD COLUMN "issuer" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "clientId" text; \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/0002_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0002_bc-wallet-api.sql new file mode 100644 index 000000000..c9df035eb --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0002_bc-wallet-api.sql @@ -0,0 +1,2 @@ +ALTER TABLE "ariesProofRequest" ALTER COLUMN "attributes" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "ariesProofRequest" ALTER COLUMN "predicates" DROP NOT NULL; \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/0003_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0003_bc-wallet-api.sql new file mode 100644 index 000000000..e8f70a3be --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0003_bc-wallet-api.sql @@ -0,0 +1,11 @@ +CREATE TABLE "tenantsToUsers" ( + "tenant" text NOT NULL, + "user" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "tenantsToUsers_tenant_user_pk" PRIMARY KEY("tenant","user") +); +--> statement-breakpoint +ALTER TABLE "tenantsToUsers" ADD CONSTRAINT "tenantsToUsers_tenant_tenant_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tenantsToUsers" ADD CONSTRAINT "tenantsToUsers_user_user_id_fk" FOREIGN KEY ("user") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "tenants_to_users_tenant_idx" ON "tenantsToUsers" USING btree ("tenant"); \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/0004_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0004_bc-wallet-api.sql new file mode 100644 index 000000000..2565d54e1 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0004_bc-wallet-api.sql @@ -0,0 +1,11 @@ +CREATE TYPE "public"."TenantType" AS ENUM('ROOT', 'SHOWCASE');--> statement-breakpoint +ALTER TABLE "credentialSchema" ALTER COLUMN "source" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "credentialDefinition" ADD COLUMN "source" "Source" DEFAULT 'CREATED' NOT NULL;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "tenantType" "TenantType" NOT NULL;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "oidc_issuer" text NOT NULL;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "traction_tenant_id" uuid;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "traction_api_url" text;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "traction_wallet_id" uuid;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "traction_api_key" text;--> statement-breakpoint +ALTER TABLE "tenant" ADD COLUMN "nonce_base_64" text;--> statement-breakpoint +CREATE INDEX "tenants_to_users_user_idx" ON "tenantsToUsers" USING btree ("user"); \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/0005_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0005_bc-wallet-api.sql new file mode 100644 index 000000000..c2abe349c --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0005_bc-wallet-api.sql @@ -0,0 +1,4 @@ +ALTER TABLE "showcase" DROP CONSTRAINT "showcase_slug_unique";--> statement-breakpoint +ALTER TABLE "credentialDefinition" ADD COLUMN "tenant_id" text NOT NULL;--> statement-breakpoint +ALTER TABLE "credentialDefinition" ADD CONSTRAINT "credentialDefinition_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "idx_tenant_slug_unique" ON "showcase" USING btree ("tenant_id","slug"); \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/0006_bc-wallet-api.sql b/apps/bc-wallet-api-server/src/database/migrations/0006_bc-wallet-api.sql new file mode 100644 index 000000000..63ec1df7a --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/0006_bc-wallet-api.sql @@ -0,0 +1,18 @@ +ALTER TABLE "scenario" DROP CONSTRAINT "scenario_type_check";--> statement-breakpoint +ALTER TABLE "scenario" DROP CONSTRAINT "scenario_issuer_issuer_id_fk"; +--> statement-breakpoint +ALTER TABLE "scenario" DROP CONSTRAINT "scenario_relying_party_relyingParty_id_fk"; +--> statement-breakpoint +ALTER TABLE "relyingParty" ADD COLUMN "tenant_id" text NOT NULL;--> statement-breakpoint +ALTER TABLE "issuer" ADD COLUMN "tenant_id" text NOT NULL;--> statement-breakpoint +ALTER TABLE "relyingParty" ADD CONSTRAINT "relyingParty_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issuer" ADD CONSTRAINT "issuer_tenant_id_tenant_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenant"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenario" ADD CONSTRAINT "scenario_issuer_issuer_id_fk" FOREIGN KEY ("issuer") REFERENCES "public"."issuer"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "scenario" ADD CONSTRAINT "scenario_relying_party_relyingParty_id_fk" FOREIGN KEY ("relying_party") REFERENCES "public"."relyingParty"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_rp_tenant" ON "relyingParty" USING btree ("tenant_id");--> statement-breakpoint +CREATE INDEX "idx_issuer_tenant" ON "issuer" USING btree ("tenant_id");--> statement-breakpoint +ALTER TABLE "scenario" ADD CONSTRAINT "scenario_type_check" CHECK ( + (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR + (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL) OR + (issuer IS NULL AND relying_party IS NULL) + ); \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0000_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0000_snapshot.json new file mode 100644 index 000000000..16b18a1be --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0000_snapshot.json @@ -0,0 +1,2400 @@ +{ + "id": "47ee3f97-3b8f-46bb-afcf-94d50f613ee9", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "showcase_slug_unique": { + "name": "showcase_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0001_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..e216154c2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0001_snapshot.json @@ -0,0 +1,2412 @@ +{ + "id": "5de42e43-3189-4d11-92fe-fd9f987c97f8", + "prevId": "47ee3f97-3b8f-46bb-afcf-94d50f613ee9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "showcase_slug_unique": { + "name": "showcase_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0002_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0002_snapshot.json new file mode 100644 index 000000000..b0d985f5f --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0002_snapshot.json @@ -0,0 +1,2412 @@ +{ + "id": "2385298c-4f7e-4589-9284-25c436cdb8e9", + "prevId": "5de42e43-3189-4d11-92fe-fd9f987c97f8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "showcase_slug_unique": { + "name": "showcase_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0003_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0003_snapshot.json new file mode 100644 index 000000000..779ac47d5 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0003_snapshot.json @@ -0,0 +1,2502 @@ +{ + "id": "44d2596e-e903-48df-b561-c315cdd77a8b", + "prevId": "2385298c-4f7e-4589-9284-25c436cdb8e9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "showcase_slug_unique": { + "name": "showcase_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenantsToUsers": { + "name": "tenantsToUsers", + "schema": "", + "columns": { + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user": { + "name": "user", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_to_users_tenant_idx": { + "name": "tenants_to_users_tenant_idx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenantsToUsers_tenant_tenant_id_fk": { + "name": "tenantsToUsers_tenant_tenant_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "tenant", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tenantsToUsers_user_user_id_fk": { + "name": "tenantsToUsers_user_user_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "user", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tenantsToUsers_tenant_user_pk": { + "name": "tenantsToUsers_tenant_user_pk", + "columns": [ + "tenant", + "user" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0004_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0004_snapshot.json new file mode 100644 index 000000000..5a7f5e77c --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0004_snapshot.json @@ -0,0 +1,2576 @@ +{ + "id": "db3d9795-ca39-49da-91f9-48e296d6d29f", + "prevId": "44d2596e-e903-48df-b561-c315cdd77a8b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenantType": { + "name": "tenantType", + "type": "TenantType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "oidc_issuer": { + "name": "oidc_issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "traction_tenant_id": { + "name": "traction_tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "traction_api_url": { + "name": "traction_api_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "traction_wallet_id": { + "name": "traction_wallet_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "traction_api_key": { + "name": "traction_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nonce_base_64": { + "name": "nonce_base_64", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "showcase_slug_unique": { + "name": "showcase_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenantsToUsers": { + "name": "tenantsToUsers", + "schema": "", + "columns": { + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user": { + "name": "user", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_to_users_tenant_idx": { + "name": "tenants_to_users_tenant_idx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tenants_to_users_user_idx": { + "name": "tenants_to_users_user_idx", + "columns": [ + { + "expression": "user", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenantsToUsers_tenant_tenant_id_fk": { + "name": "tenantsToUsers_tenant_tenant_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "tenant", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tenantsToUsers_user_user_id_fk": { + "name": "tenantsToUsers_user_user_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "user", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tenantsToUsers_tenant_user_pk": { + "name": "tenantsToUsers_tenant_user_pk", + "columns": [ + "tenant", + "user" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.TenantType": { + "name": "TenantType", + "schema": "public", + "values": [ + "ROOT", + "SHOWCASE" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0005_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0005_snapshot.json new file mode 100644 index 000000000..cac4abfcb --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0005_snapshot.json @@ -0,0 +1,2608 @@ +{ + "id": "5c98b866-df93-4606-8f06-2d5facd91728", + "prevId": "db3d9795-ca39-49da-91f9-48e296d6d29f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_tenant_id_tenant_id_fk": { + "name": "credentialDefinition_tenant_id_tenant_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenantType": { + "name": "tenantType", + "type": "TenantType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "oidc_issuer": { + "name": "oidc_issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "traction_tenant_id": { + "name": "traction_tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "traction_api_url": { + "name": "traction_api_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "traction_wallet_id": { + "name": "traction_wallet_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "traction_api_key": { + "name": "traction_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nonce_base_64": { + "name": "nonce_base_64", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_slug_unique": { + "name": "idx_tenant_slug_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenantsToUsers": { + "name": "tenantsToUsers", + "schema": "", + "columns": { + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user": { + "name": "user", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_to_users_tenant_idx": { + "name": "tenants_to_users_tenant_idx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tenants_to_users_user_idx": { + "name": "tenants_to_users_user_idx", + "columns": [ + { + "expression": "user", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenantsToUsers_tenant_tenant_id_fk": { + "name": "tenantsToUsers_tenant_tenant_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "tenant", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tenantsToUsers_user_user_id_fk": { + "name": "tenantsToUsers_user_user_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "user", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tenantsToUsers_tenant_user_pk": { + "name": "tenantsToUsers_tenant_user_pk", + "columns": [ + "tenant", + "user" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.TenantType": { + "name": "TenantType", + "schema": "public", + "values": [ + "ROOT", + "SHOWCASE" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/0006_snapshot.json b/apps/bc-wallet-api-server/src/database/migrations/meta/0006_snapshot.json new file mode 100644 index 000000000..786d903b3 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/0006_snapshot.json @@ -0,0 +1,2676 @@ +{ + "id": "f3f2c553-9aff-4773-aa73-e9d6de3a6901", + "prevId": "5c98b866-df93-4606-8f06-2d5facd91728", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ariesProofRequest": { + "name": "ariesProofRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "predicates": { + "name": "predicates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "step_action": { + "name": "step_action", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ariesProofRequest_step_action_stepAction_id_fk": { + "name": "ariesProofRequest_step_action_stepAction_id_fk", + "tableFrom": "ariesProofRequest", + "tableTo": "stepAction", + "columnsFrom": [ + "step_action" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ariesProofRequest_step_action_unique": { + "name": "ariesProofRequest_step_action_unique", + "nullsNotDistinct": false, + "columns": [ + "step_action" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialAttribute": { + "name": "credentialAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "CredentialAttributeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_attributes": { + "name": "idx_credential_attributes", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialAttribute_credential_schema_credentialSchema_id_fk": { + "name": "credentialAttribute_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialAttribute", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialDefinition": { + "name": "credentialDefinition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "CredentialType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cd_icon": { + "name": "idx_cd_icon", + "columns": [ + { + "expression": "icon", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_credentialSchema": { + "name": "idx_cd_credentialSchema", + "columns": [ + { + "expression": "credential_schema", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cd_approvedBy": { + "name": "idx_cd_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialDefinition_tenant_id_tenant_id_fk": { + "name": "credentialDefinition_tenant_id_tenant_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credentialDefinition_credential_schema_credentialSchema_id_fk": { + "name": "credentialDefinition_credential_schema_credentialSchema_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_icon_asset_id_fk": { + "name": "credentialDefinition_icon_asset_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "asset", + "columnsFrom": [ + "icon" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credentialDefinition_approved_by_user_id_fk": { + "name": "credentialDefinition_approved_by_user_id_fk", + "tableFrom": "credentialDefinition", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialRepresentation": { + "name": "credentialRepresentation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_credential_representations": { + "name": "idx_credential_representations", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credentialRepresentation_credential_definition_credentialDefinition_id_fk": { + "name": "credentialRepresentation_credential_definition_credentialDefinition_id_fk", + "tableFrom": "credentialRepresentation", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentialSchema": { + "name": "credentialSchema", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier_type": { + "name": "identifier_type", + "type": "IdentifierType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "Source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CREATED'" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingPartiesToCredentialDefinitions": { + "name": "relyingPartiesToCredentialDefinitions", + "schema": "", + "columns": { + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty": { + "name": "idx_relyingParty", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_relyingParty_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "relyingPartiesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "relyingPartiesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk": { + "name": "relyingPartiesToCredentialDefinitions_relying_party_credential_definition_pk", + "columns": [ + "relying_party", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.relyingParty": { + "name": "relyingParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "RelyingPartyType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_logo": { + "name": "idx_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rp_tenant": { + "name": "idx_rp_tenant", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "relyingParty_logo_asset_id_fk": { + "name": "relyingParty_logo_asset_id_fk", + "tableFrom": "relyingParty", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "relyingParty_tenant_id_tenant_id_fk": { + "name": "relyingParty_tenant_id_tenant_id_fk", + "tableFrom": "relyingParty", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuer": { + "name": "issuer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "IssuerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer_logo": { + "name": "idx_issuer_logo", + "columns": [ + { + "expression": "logo", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_tenant": { + "name": "idx_issuer_tenant", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuer_logo_asset_id_fk": { + "name": "issuer_logo_asset_id_fk", + "tableFrom": "issuer", + "tableTo": "asset", + "columnsFrom": [ + "logo" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issuer_tenant_id_tenant_id_fk": { + "name": "issuer_tenant_id_tenant_id_fk", + "tableFrom": "issuer", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialDefinitions": { + "name": "issuersToCredentialDefinitions", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_issuer": { + "name": "idx_issuer", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialDefinitions_issuer_issuer_id_fk": { + "name": "issuersToCredentialDefinitions_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "issuersToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "issuersToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialDefinitions_issuer_credential_definition_pk": { + "name": "issuersToCredentialDefinitions_issuer_credential_definition_pk", + "columns": [ + "issuer", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issuersToCredentialSchemas": { + "name": "issuersToCredentialSchemas", + "schema": "", + "columns": { + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_schema": { + "name": "credential_schema", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_issuer_schemas": { + "name": "idx_issuer_schemas", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issuersToCredentialSchemas_issuer_issuer_id_fk": { + "name": "issuersToCredentialSchemas_issuer_issuer_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk": { + "name": "issuersToCredentialSchemas_credential_schema_credentialSchema_id_fk", + "tableFrom": "issuersToCredentialSchemas", + "tableTo": "credentialSchema", + "columnsFrom": [ + "credential_schema" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issuersToCredentialSchemas_issuer_credential_schema_pk": { + "name": "issuersToCredentialSchemas_issuer_credential_schema_pk", + "columns": [ + "issuer", + "credential_schema" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step": { + "name": "step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "StepType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sub_scenario": { + "name": "sub_scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset": { + "name": "asset", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenarios_steps": { + "name": "idx_scenarios_steps", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_asset_steps": { + "name": "idx_asset_steps", + "columns": [ + { + "expression": "asset", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "step_sub_scenario_scenario_id_fk": { + "name": "step_sub_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "sub_scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "step_scenario_scenario_id_fk": { + "name": "step_scenario_scenario_id_fk", + "tableFrom": "step", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "step_asset_asset_id_fk": { + "name": "step_asset_asset_id_fk", + "tableFrom": "step", + "tableTo": "asset", + "columnsFrom": [ + "asset" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "step_order_scenario_unique": { + "name": "step_order_scenario_unique", + "nullsNotDistinct": false, + "columns": [ + "order", + "scenario" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stepAction": { + "name": "stepAction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step": { + "name": "step", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credential_definition_id": { + "name": "credential_definition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "go_to_step": { + "name": "go_to_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_step_actions": { + "name": "idx_step_actions", + "columns": [ + { + "expression": "step", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stepAction_step_step_id_fk": { + "name": "stepAction_step_step_id_fk", + "tableFrom": "stepAction", + "tableTo": "step", + "columnsFrom": [ + "step" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "ScenarioType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "relying_party": { + "name": "relying_party", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_relyingParty_issuer_scenario": { + "name": "idx_relyingParty_issuer_scenario", + "columns": [ + { + "expression": "relying_party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issuer_scenario": { + "name": "idx_issuer_scenario", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bannerImage_issuer_scenario": { + "name": "idx_bannerImage_issuer_scenario", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenario_issuer_issuer_id_fk": { + "name": "scenario_issuer_issuer_id_fk", + "tableFrom": "scenario", + "tableTo": "issuer", + "columnsFrom": [ + "issuer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scenario_relying_party_relyingParty_id_fk": { + "name": "scenario_relying_party_relyingParty_id_fk", + "tableFrom": "scenario", + "tableTo": "relyingParty", + "columnsFrom": [ + "relying_party" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scenario_banner_image_asset_id_fk": { + "name": "scenario_banner_image_asset_id_fk", + "tableFrom": "scenario", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scenario_slug_unique": { + "name": "scenario_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scenario_type_check": { + "name": "scenario_type_check", + "value": "\n (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR\n (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL) OR\n (issuer IS NULL AND relying_party IS NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.revocationInfo": { + "name": "revocationInfo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_revocation_info": { + "name": "idx_revocation_info", + "columns": [ + { + "expression": "credential_definition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "revocationInfo_credential_definition_credentialDefinition_id_fk": { + "name": "revocationInfo_credential_definition_credentialDefinition_id_fk", + "tableFrom": "revocationInfo", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "revocationInfo_credential_definition_unique": { + "name": "revocationInfo_credential_definition_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_definition" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.persona": { + "name": "persona", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headshot_image": { + "name": "headshot_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body_image": { + "name": "body_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_headshotImage": { + "name": "idx_headshotImage", + "columns": [ + { + "expression": "headshot_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bodyImage": { + "name": "idx_bodyImage", + "columns": [ + { + "expression": "body_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "persona_headshot_image_asset_id_fk": { + "name": "persona_headshot_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "headshot_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "persona_body_image_asset_id_fk": { + "name": "persona_body_image_asset_id_fk", + "tableFrom": "persona", + "tableTo": "asset", + "columnsFrom": [ + "body_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "persona_slug_unique": { + "name": "persona_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenariosToPersonas": { + "name": "scenariosToPersonas", + "schema": "", + "columns": { + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_scenario": { + "name": "idx_scenario", + "columns": [ + { + "expression": "scenario", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scenariosToPersonas_scenario_scenario_id_fk": { + "name": "scenariosToPersonas_scenario_scenario_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scenariosToPersonas_persona_persona_id_fk": { + "name": "scenariosToPersonas_persona_persona_id_fk", + "tableFrom": "scenariosToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scenariosToPersonas_scenario_persona_pk": { + "name": "scenariosToPersonas_scenario_persona_pk", + "columns": [ + "scenario", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenantType": { + "name": "tenantType", + "type": "TenantType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "oidc_issuer": { + "name": "oidc_issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "traction_tenant_id": { + "name": "traction_tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "traction_api_url": { + "name": "traction_api_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "traction_wallet_id": { + "name": "traction_wallet_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "traction_api_key": { + "name": "traction_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nonce_base_64": { + "name": "nonce_base_64", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcase": { + "name": "showcase", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completionMessage": { + "name": "completionMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ShowcaseStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banner_image": { + "name": "banner_image", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bannerImage": { + "name": "idx_bannerImage", + "columns": [ + { + "expression": "banner_image", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_id": { + "name": "idx_tenant_id", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sc_approvedBy": { + "name": "idx_sc_approvedBy", + "columns": [ + { + "expression": "approved_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tenant_slug_unique": { + "name": "idx_tenant_slug_unique", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcase_tenant_id_tenant_id_fk": { + "name": "showcase_tenant_id_tenant_id_fk", + "tableFrom": "showcase", + "tableTo": "tenant", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcase_banner_image_asset_id_fk": { + "name": "showcase_banner_image_asset_id_fk", + "tableFrom": "showcase", + "tableTo": "asset", + "columnsFrom": [ + "banner_image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_approved_by_user_id_fk": { + "name": "showcase_approved_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "showcase_created_by_user_id_fk": { + "name": "showcase_created_by_user_id_fk", + "tableFrom": "showcase", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToCredentialDefinitions": { + "name": "showcasesToCredentialDefinitions", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_definition": { + "name": "credential_definition", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_credential_definitions_showcase_idx": { + "name": "showcases_to_credential_definitions_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToCredentialDefinitions_showcase_showcase_id_fk": { + "name": "showcasesToCredentialDefinitions_showcase_showcase_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk": { + "name": "showcasesToCredentialDefinitions_credential_definition_credentialDefinition_id_fk", + "tableFrom": "showcasesToCredentialDefinitions", + "tableTo": "credentialDefinition", + "columnsFrom": [ + "credential_definition" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToCredentialDefinitions_showcase_credential_definition_pk": { + "name": "showcasesToCredentialDefinitions_showcase_credential_definition_pk", + "columns": [ + "showcase", + "credential_definition" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToPersonas": { + "name": "showcasesToPersonas", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_personas_showcase_idx": { + "name": "showcases_to_personas_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToPersonas_showcase_showcase_id_fk": { + "name": "showcasesToPersonas_showcase_showcase_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToPersonas_persona_persona_id_fk": { + "name": "showcasesToPersonas_persona_persona_id_fk", + "tableFrom": "showcasesToPersonas", + "tableTo": "persona", + "columnsFrom": [ + "persona" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToPersonas_showcase_persona_pk": { + "name": "showcasesToPersonas_showcase_persona_pk", + "columns": [ + "showcase", + "persona" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.showcasesToScenarios": { + "name": "showcasesToScenarios", + "schema": "", + "columns": { + "showcase": { + "name": "showcase", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scenario": { + "name": "scenario", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "showcases_to_scenarios_showcase_idx": { + "name": "showcases_to_scenarios_showcase_idx", + "columns": [ + { + "expression": "showcase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "showcasesToScenarios_showcase_showcase_id_fk": { + "name": "showcasesToScenarios_showcase_showcase_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "showcase", + "columnsFrom": [ + "showcase" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "showcasesToScenarios_scenario_scenario_id_fk": { + "name": "showcasesToScenarios_scenario_scenario_id_fk", + "tableFrom": "showcasesToScenarios", + "tableTo": "scenario", + "columnsFrom": [ + "scenario" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "showcasesToScenarios_showcase_scenario_pk": { + "name": "showcasesToScenarios_showcase_scenario_pk", + "columns": [ + "showcase", + "scenario" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "userName": { + "name": "userName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenantsToUsers": { + "name": "tenantsToUsers", + "schema": "", + "columns": { + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user": { + "name": "user", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tenants_to_users_tenant_idx": { + "name": "tenants_to_users_tenant_idx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tenants_to_users_user_idx": { + "name": "tenants_to_users_user_idx", + "columns": [ + { + "expression": "user", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenantsToUsers_tenant_tenant_id_fk": { + "name": "tenantsToUsers_tenant_tenant_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "tenant", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tenantsToUsers_user_user_id_fk": { + "name": "tenantsToUsers_user_user_id_fk", + "tableFrom": "tenantsToUsers", + "tableTo": "user", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tenantsToUsers_tenant_user_pk": { + "name": "tenantsToUsers_tenant_user_pk", + "columns": [ + "tenant", + "user" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.CredentialAttributeType": { + "name": "CredentialAttributeType", + "schema": "public", + "values": [ + "STRING", + "INTEGER", + "FLOAT", + "BOOLEAN", + "DATE" + ] + }, + "public.CredentialType": { + "name": "CredentialType", + "schema": "public", + "values": [ + "ANONCRED" + ] + }, + "public.IdentifierType": { + "name": "IdentifierType", + "schema": "public", + "values": [ + "DID" + ] + }, + "public.IssuerType": { + "name": "IssuerType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.StepType": { + "name": "StepType", + "schema": "public", + "values": [ + "HUMAN_TASK", + "SERVICE", + "SCENARIO" + ] + }, + "public.ScenarioType": { + "name": "ScenarioType", + "schema": "public", + "values": [ + "ISSUANCE", + "PRESENTATION" + ] + }, + "public.TenantType": { + "name": "TenantType", + "schema": "public", + "values": [ + "ROOT", + "SHOWCASE" + ] + }, + "public.ShowcaseStatus": { + "name": "ShowcaseStatus", + "schema": "public", + "values": [ + "PENDING", + "ACTIVE", + "ARCHIVED" + ] + }, + "public.RelyingPartyType": { + "name": "RelyingPartyType", + "schema": "public", + "values": [ + "ARIES" + ] + }, + "public.Source": { + "name": "Source", + "schema": "public", + "values": [ + "IMPORTED", + "CREATED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/migrations/meta/_journal.json b/apps/bc-wallet-api-server/src/database/migrations/meta/_journal.json new file mode 100644 index 000000000..1abbd8bb9 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/migrations/meta/_journal.json @@ -0,0 +1,55 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1744213708828, + "tag": "0000_bc-wallet-api", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1744706515544, + "tag": "0001_bc-wallet-api", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1744798874845, + "tag": "0002_bc-wallet-api", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1744896983917, + "tag": "0003_bc-wallet-api", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1747090254184, + "tag": "0004_bc-wallet-api", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1747333525632, + "tag": "0005_bc-wallet-api", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1752847843589, + "tag": "0006_bc-wallet-api", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/bc-wallet-api-server/src/database/repositories/AssetRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/AssetRepository.ts new file mode 100644 index 000000000..fb78a3952 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/AssetRepository.ts @@ -0,0 +1,50 @@ +import { eq } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { Asset, NewAsset, RepositoryDefinition } from '../../types' +import { assets } from '../schema' + +@Service() +class AssetRepository implements RepositoryDefinition { + constructor(private readonly databaseService: DatabaseService) {} + + async create(asset: NewAsset): Promise { + const [result] = await (await this.databaseService.getConnection()).insert(assets).values(asset).returning() + + return result + } + + async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(assets).where(eq(assets.id, id)) + } + + async update(id: string, asset: NewAsset): Promise { + await this.findById(id) + const [result] = await (await this.databaseService.getConnection()) + .update(assets) + .set(asset) + .where(eq(assets.id, id)) + .returning() + + return result + } + + async findById(id: string): Promise { + const [result] = await (await this.databaseService.getConnection()).select().from(assets).where(eq(assets.id, id)) + + if (!result) { + return Promise.reject(new NotFoundError(`No asset found for id: ${id}`)) + } + + return result + } + + async findAll(): Promise { + return (await this.databaseService.getConnection()).select().from(assets) + } +} + +export default AssetRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/CredentialDefinitionRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/CredentialDefinitionRepository.ts new file mode 100644 index 000000000..31eb90b3e --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/CredentialDefinitionRepository.ts @@ -0,0 +1,305 @@ +import { and, eq, isNull } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { + CredentialDefinition, + IdentifierType, + NewCredentialDefinition, + TenantScopedRepositoryDefinition, + Tx, +} from '../../types' +import { credentialDefinitions, credentialRepresentations, revocationInfo } from '../schema' +import AssetRepository from './AssetRepository' +import CredentialSchemaRepository from './CredentialSchemaRepository' + +@Service() +class CredentialDefinitionRepository + implements TenantScopedRepositoryDefinition +{ + public constructor( + private readonly databaseService: DatabaseService, + private readonly assetRepository: AssetRepository, + private readonly credentialSchemaRepository: CredentialSchemaRepository, + ) {} + + public async create(credentialDefinition: NewCredentialDefinition): Promise { + const iconResult = credentialDefinition.icon && (await this.assetRepository.findById(credentialDefinition.icon)) + const credentialSchemaResult = await this.credentialSchemaRepository.findById(credentialDefinition.credentialSchema) + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [credentialDefinitionResult] = await tx + .insert(credentialDefinitions) + .values(credentialDefinition) + .returning() + + // TODO SHOWCASE-81 enable + // const credentialRepresentationsResult = await tx.insert(credentialRepresentations) + // .values(credentialDefinition.representations.map((representation: NewCredentialRepresentation) => ({ + // ...representation, + // credentialDefinition: credentialDefinitionResult.id + // }))) + // .returning(); + + // TODO SHOWCASE-80 enable + let revocationResult = null + // if (credentialDefinition.revocation) { + // [revocationResult] = await tx.insert(revocationInfo) + // .values({ + // ...credentialDefinition.revocation, + // credentialDefinition: credentialDefinitionResult.id + // }) + // .returning(); + // } + + return { + ...credentialDefinitionResult, + credentialSchema: credentialSchemaResult, + icon: iconResult ? iconResult : undefined, + representations: [], //credentialRepresentationsResult, TODO SHOWCASE-81 enable + revocation: revocationResult, + approvedBy: null, + approvedAt: null, + } + }) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()) + .delete(credentialDefinitions) + .where(eq(credentialDefinitions.id, id)) + } + + public async update(id: string, credentialDefinition: NewCredentialDefinition): Promise { + await this.findById(id) + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + await tx.update(credentialDefinitions).set(credentialDefinition).where(eq(credentialDefinitions.id, id)) + + await tx.delete(credentialRepresentations).where(eq(credentialRepresentations.credentialDefinition, id)) + await tx.delete(revocationInfo).where(eq(revocationInfo.credentialDefinition, id)) + + // TODO SHOWCASE-81 enable + // const credentialRepresentationsResult = await tx.insert(credentialRepresentations) + // .values(credentialDefinition.representations.map((representation: NewCredentialRepresentation) => ({ + // ...representation, + // credentialDefinition: credentialDefinitionResult.id + // }))) + // .returning(); + + // TODO SHOWCASE-80 enable + //let revocationResult = null + // if (credentialDefinition.revocation) { + // [revocationResult] = await tx.insert(revocationInfo) + // .values({ + // ...credentialDefinition.revocation, + // credentialDefinition: credentialDefinitionResult.id + // }) + // .returning(); + // } + + // 3. Re-fetch the complete record *using the transaction object 'tx'* + const updatedRecord = await tx.query.credentialDefinitions.findFirst({ + where: eq(credentialDefinitions.id, id), + with: { + icon: true, + cs: { + // Fetch the schema relation + with: { + attributes: true, + }, + }, + approver: true, + // representations: true, // Fetch representations if managed here and relation exists + // revocation: true, // Fetch revocation if managed here and relation exists + }, + }) + + if (!updatedRecord) { + // Should not happen, but makes ts happy + return Promise.reject(new Error(`Failed to re-fetch credential definition after update: ${id}`)) + } + + return { + // Map direct columns from updatedRecord + id: updatedRecord.id, + tenantId: updatedRecord.tenantId, + name: updatedRecord.name, + version: updatedRecord.version, + identifierType: updatedRecord.identifierType, + identifier: updatedRecord.identifier, + type: updatedRecord.type, + source: updatedRecord.source, + createdAt: updatedRecord.createdAt, + updatedAt: updatedRecord.updatedAt, + approvedAt: updatedRecord.approvedAt, + + // Map related objects from the fetch + // Use the objects fetched *during the re-fetch* inside the transaction + credentialSchema: { + ...updatedRecord.cs, + attributes: updatedRecord.cs.attributes ?? [], + }, + icon: updatedRecord.icon ?? undefined, + representations: [], // updatedRecord.representations, + revocation: null, // updatedRecord.revocation, + approvedBy: updatedRecord.approver, + } + }) + } + + public async findById(id: string, tenantId?: string, tx?: Tx): Promise { + const whereCondition = tenantId + ? and(eq(credentialDefinitions.id, id), eq(credentialDefinitions.tenantId, tenantId)) + : eq(credentialDefinitions.id, id) + + const result = await (tx ?? (await this.databaseService.getConnection())).query.credentialDefinitions.findFirst({ + where: whereCondition, + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No credential definition found for id: ${id}`)) + } + + return { + ...result, + icon: result.icon ? result.icon : undefined, + credentialSchema: result.cs, + approvedBy: result.approver, + } + } + + public async findByIdentifier( + identifier: string, + identifierType?: IdentifierType, + tx?: Tx, + ): Promise { + const whereConditions = [eq(credentialDefinitions.identifier, identifier)] + if (identifierType !== undefined) { + whereConditions.push(eq(credentialDefinitions.identifierType, identifierType)) + } + + const result = await (tx ?? (await this.databaseService.getConnection())).query.credentialDefinitions.findFirst({ + where: and(...whereConditions), + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No credential definition found for identifier: ${identifier}`)) + } + + return { + ...result, + icon: result.icon ? result.icon : undefined, + credentialSchema: result.cs, + approvedBy: result.approver, + } + } + + public async findAll(tenantId?: string): Promise { + const whereCondition = tenantId ? eq(credentialDefinitions.tenantId, tenantId) : undefined + + const result = await ( + await this.databaseService.getConnection() + ).query.credentialDefinitions.findMany({ + where: whereCondition, + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }) + + return result.map((item: any) => ({ + ...item, + credentialSchema: item.cs, + approvedBy: item.approver, + })) + } + + public async findUnapproved(tenantId?: string): Promise { + const whereCondition = tenantId + ? and(isNull(credentialDefinitions.approvedAt), eq(credentialDefinitions.tenantId, tenantId)) + : isNull(credentialDefinitions.approvedAt) + + const result = await ( + await this.databaseService.getConnection() + ).query.credentialDefinitions.findMany({ + where: whereCondition, + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }) + + return result.map( + (item): CredentialDefinition => ({ + ...item, + credentialSchema: { + ...item.cs, + attributes: item.cs.attributes ?? [], + }, + icon: item.icon ?? undefined, + representations: item.representations ?? [], + revocation: item.revocation, + approvedBy: null, + approvedAt: null, + }), + ) + } + + public async approve(id: string, userId: string, tenantId?: string): Promise { + const now = new Date() + + const connection = await this.databaseService.getConnection() + return await connection.transaction(async (tx) => { + await tx + .update(credentialDefinitions) + .set({ + approvedBy: userId, + approvedAt: now, + updatedAt: now, + }) + .where(eq(credentialDefinitions.id, id)) + return await this.findById(id, tenantId, tx) + }) + } +} + +export default CredentialDefinitionRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/CredentialSchemaRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/CredentialSchemaRepository.ts new file mode 100644 index 000000000..f41663cd0 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/CredentialSchemaRepository.ts @@ -0,0 +1,127 @@ +import { and, eq } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { + CredentialAttribute, + CredentialSchema, + IdentifierType, + NewCredentialAttribute, + NewCredentialSchema, + RepositoryDefinition, +} from '../../types' +import { credentialAttributes, credentialSchemas } from '../schema' + +@Service() +class CredentialSchemaRepository implements RepositoryDefinition { + public constructor(private readonly databaseService: DatabaseService) {} + + public async create(credentialSchema: NewCredentialSchema): Promise { + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [credentialSchemaResult] = await tx.insert(credentialSchemas).values(credentialSchema).returning() + + let credentialAttributesResult: CredentialAttribute[] = [] + if (credentialSchema.attributes && credentialSchema.attributes.length > 0) { + credentialAttributesResult = await tx + .insert(credentialAttributes) + .values( + credentialSchema.attributes.map((attribute: NewCredentialAttribute) => ({ + ...attribute, + credentialSchema: credentialSchemaResult.id, + })), + ) + .returning() + } + + return { + ...credentialSchemaResult, + attributes: credentialAttributesResult, + } + }) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(credentialSchemas).where(eq(credentialSchemas.id, id)) + } + + public async update(id: string, credentialSchema: NewCredentialSchema): Promise { + await this.findById(id) + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [credentialSchemaResult] = await tx + .update(credentialSchemas) + .set(credentialSchema) + .where(eq(credentialSchemas.id, id)) + .returning() + + await tx.delete(credentialAttributes).where(eq(credentialAttributes.credentialSchema, id)) + + const credentialAttributesResult = await tx + .insert(credentialAttributes) + .values( + credentialSchema.attributes.map((attribute: NewCredentialAttribute) => ({ + ...attribute, + credentialSchema: credentialSchemaResult.id, + })), + ) + .returning() + + return { + ...credentialSchemaResult, + attributes: credentialAttributesResult, + } + }) + } + + public async findById(id: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.credentialSchemas.findFirst({ + where: eq(credentialSchemas.id, id), + with: { + attributes: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No credential schema found for id: ${id}`)) + } + + return result + } + + public async findByIdentifier(identifier: string, identifierType?: IdentifierType): Promise { + const whereConditions = [eq(credentialSchemas.identifier, identifier)] + + if (identifierType !== undefined) { + whereConditions.push(eq(credentialSchemas.identifierType, identifierType)) + } + + const result = await ( + await this.databaseService.getConnection() + ).query.credentialSchemas.findFirst({ + where: and(...whereConditions), + with: { + attributes: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No credential schema found for identifier: ${identifier}`)) + } + + return result + } + + public async findAll(): Promise { + return (await this.databaseService.getConnection()).query.credentialSchemas.findMany({ + with: { + attributes: true, + }, + }) + } +} + +export default CredentialSchemaRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/IssuerRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/IssuerRepository.ts new file mode 100644 index 000000000..5b11b613d --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/IssuerRepository.ts @@ -0,0 +1,300 @@ +import { eq, inArray } from 'drizzle-orm' +import { BadRequestError, NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { Issuer, NewIssuer, RepositoryDefinition } from '../../types' +import { + credentialDefinitions, + credentialSchemas, + issuers, + issuersToCredentialDefinitions, + issuersToCredentialSchemas, +} from '../schema' +import AssetRepository from './AssetRepository' +import CredentialDefinitionRepository from './CredentialDefinitionRepository' +import CredentialSchemaRepository from './CredentialSchemaRepository' + +@Service() +class IssuerRepository implements RepositoryDefinition { + public constructor( + private readonly databaseService: DatabaseService, + private readonly credentialDefinitionRepository: CredentialDefinitionRepository, + private readonly credentialSchemaRepository: CredentialSchemaRepository, + private readonly assetRepository: AssetRepository, + ) {} + + public async create(issuer: NewIssuer): Promise { + const credentialDefinitionPromises = issuer.credentialDefinitions.map( + async (credentialDefinition) => await this.credentialDefinitionRepository.findById(credentialDefinition), + ) + await Promise.all(credentialDefinitionPromises) + const credentialSchemaPromises = issuer.credentialSchemas.map( + async (credentialDefinition) => await this.credentialSchemaRepository.findById(credentialDefinition), + ) + await Promise.all(credentialSchemaPromises) + const logoResult = issuer.logo ? await this.assetRepository.findById(issuer.logo) : null + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [issuerResult] = await tx + .insert(issuers) + .values({ + ...issuer, + logo: logoResult ? logoResult.id : null, + }) + .returning() + + let credentialDefinitionsResult: any[] = [] + let credentialSchemasResult: any[] = [] + + // Only insert if arrays are not empty + if (issuer.credentialDefinitions.length > 0) { + const issuersToCredentialDefinitionsResult = await tx + .insert(issuersToCredentialDefinitions) + .values( + issuer.credentialDefinitions.map((credentialDefinitionId: string) => ({ + issuer: issuerResult.id, + credentialDefinition: credentialDefinitionId, + })), + ) + .returning() + + credentialDefinitionsResult = await tx.query.credentialDefinitions.findMany({ + where: inArray( + credentialDefinitions.id, + issuersToCredentialDefinitionsResult.map((item) => item.credentialDefinition), + ), + with: { + cs: { with: { attributes: true } }, + representations: true, + revocation: true, + icon: true, + }, + }) + } + + if (issuer.credentialSchemas.length > 0) { + const issuersToCredentialSchemasResult = await tx + .insert(issuersToCredentialSchemas) + .values( + issuer.credentialSchemas.map((credentialSchema: string) => ({ + issuer: issuerResult.id, + credentialSchema: credentialSchema, + })), + ) + .returning() + + credentialSchemasResult = await tx.query.credentialSchemas.findMany({ + where: inArray( + credentialSchemas.id, + issuersToCredentialSchemasResult.map((item) => item.credentialSchema), + ), + with: { attributes: true }, + }) + } + + return { + ...issuerResult, + logo: logoResult, + credentialDefinitions: credentialDefinitionsResult.map((item: any) => ({ + ...item, + credentialSchema: item.cs, + })), + credentialSchemas: credentialSchemasResult, + } + }) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(issuers).where(eq(issuers.id, id)) + } + + public async update(id: string, issuer: NewIssuer): Promise { + await this.findById(id) + + if (issuer.credentialDefinitions.length === 0) { + return Promise.reject(new BadRequestError('At least one credential definition is required')) + } + if (issuer.credentialSchemas.length === 0) { + return Promise.reject(new BadRequestError('At least one credential schema is required')) + } + + const credentialDefinitionPromises = issuer.credentialDefinitions.map( + async (credentialDefinition) => await this.credentialDefinitionRepository.findById(credentialDefinition), + ) + await Promise.all(credentialDefinitionPromises) + const credentialSchemaPromises = issuer.credentialSchemas.map( + async (credentialDefinition) => await this.credentialSchemaRepository.findById(credentialDefinition), + ) + await Promise.all(credentialSchemaPromises) + const logoResult = issuer.logo ? await this.assetRepository.findById(issuer.logo) : null + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [issuerResult] = await tx + .update(issuers) + .set({ + ...issuer, + logo: logoResult ? logoResult.id : null, + }) + .where(eq(issuers.id, id)) + .returning() + + await tx.delete(issuersToCredentialDefinitions).where(eq(issuersToCredentialDefinitions.issuer, id)) + await tx.delete(issuersToCredentialSchemas).where(eq(issuersToCredentialSchemas.issuer, id)) + + const issuersToCredentialDefinitionsResult = await tx + .insert(issuersToCredentialDefinitions) + .values( + issuer.credentialDefinitions.map((credentialDefinitionId: string) => ({ + issuer: issuerResult.id, + credentialDefinition: credentialDefinitionId, + })), + ) + .returning() + + const credentialDefinitionsResult = await tx.query.credentialDefinitions.findMany({ + where: inArray( + credentialDefinitions.id, + issuersToCredentialDefinitionsResult.map((item) => item.credentialDefinition), + ), + with: { + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + icon: true, + }, + }) + + const issuersToCredentialSchemasResult = await tx + .insert(issuersToCredentialSchemas) + .values( + issuer.credentialSchemas.map((credentialSchema: string) => ({ + issuer: issuerResult.id, + credentialSchema: credentialSchema, + })), + ) + .returning() + + const credentialSchemasResult = await tx.query.credentialSchemas.findMany({ + where: inArray( + credentialSchemas.id, + issuersToCredentialSchemasResult.map((item) => item.credentialSchema), + ), + with: { + attributes: true, + }, + }) + + return { + ...issuerResult, + logo: logoResult, + credentialDefinitions: credentialDefinitionsResult.map((item: any) => ({ + ...item, + credentialSchema: item.cs, + })), + credentialSchemas: credentialSchemasResult, + } + }) + } + + public async findById(id: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.issuers.findFirst({ + where: eq(issuers.id, id), + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No issuer found for id: ${id}`)) + } + + return { + ...result, + credentialDefinitions: result.cds.map((item: any) => ({ + ...item.cd, + credentialSchema: item.cd.cs, + })), + credentialSchemas: result.css.map((item) => item.cs), + } + } + + public async findAll(): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.issuers.findMany({ + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }) + + return result.map((issuer) => ({ + ...issuer, + tenant: issuer.tenantId ? { id: issuer.tenantId } : null, + credentialDefinitions: issuer.cds.map((item: any) => ({ + ...item.cd, + credentialSchema: item.cd.cs, + })), + credentialSchemas: issuer.css.map((item) => item.cs), + })) + } +} + +export default IssuerRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/PersonaRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/PersonaRepository.ts new file mode 100644 index 000000000..eb094a06c --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/PersonaRepository.ts @@ -0,0 +1,130 @@ +import { eq } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { NewPersona, Persona, RepositoryDefinition, Tx } from '../../types' +import { generateSlug } from '../../utils/slug' +import { personas } from '../schema' +import AssetRepository from './AssetRepository' + +@Service() +class PersonaRepository implements RepositoryDefinition { + constructor( + private readonly databaseService: DatabaseService, + private readonly assetRepository: AssetRepository, + ) {} + + async create(persona: NewPersona): Promise { + const headshotImageResult = persona.headshotImage + ? await this.assetRepository.findById(persona.headshotImage) + : null + const bodyImageResult = persona.bodyImage ? await this.assetRepository.findById(persona.bodyImage) : null + const connection = await this.databaseService.getConnection() + const slug = await generateSlug({ + value: persona.name, + connection, + schema: personas, + }) + + const [result] = await connection + .insert(personas) + .values({ + ...persona, + slug, + headshotImage: headshotImageResult ? headshotImageResult.id : null, + bodyImage: bodyImageResult ? bodyImageResult.id : null, + }) + .returning() + + return { + ...result, + bodyImage: bodyImageResult, + headshotImage: headshotImageResult, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + hidden: result.hidden, + } + } + + async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(personas).where(eq(personas.id, id)) + } + + async update(id: string, persona: NewPersona): Promise { + await this.findById(id) + const headshotImageResult = persona.headshotImage + ? await this.assetRepository.findById(persona.headshotImage) + : null + const bodyImageResult = persona.bodyImage ? await this.assetRepository.findById(persona.bodyImage) : null + const connection = await this.databaseService.getConnection() + const slug = await generateSlug({ + value: persona.name, + id, + connection, + schema: personas, + }) + + const [result] = await connection + .update(personas) + .set({ + ...persona, + slug, + headshotImage: headshotImageResult ? headshotImageResult.id : null, + bodyImage: bodyImageResult ? bodyImageResult.id : null, + }) + .where(eq(personas.id, id)) + .returning() + + return { + ...result, + bodyImage: bodyImageResult, + headshotImage: headshotImageResult, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + hidden: result.hidden, + } + } + + async findById(id: string, tx?: Tx): Promise { + const result = await (tx ?? (await this.databaseService.getConnection())).query.personas.findFirst({ + where: eq(personas.id, id), + with: { + headshotImage: true, + bodyImage: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No persona found for id: ${id}`)) + } + + return result + } + + async findAll(): Promise { + return (await this.databaseService.getConnection()).query.personas.findMany({ + with: { + headshotImage: true, + bodyImage: true, + }, + }) + } + + async findIdBySlug(slug: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.personas.findFirst({ + where: eq(personas.slug, slug), + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No persona found for slug: ${slug}`)) + } + + return result.id + } +} + +export default PersonaRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/RelyingPartyRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/RelyingPartyRepository.ts new file mode 100644 index 000000000..9261436ea --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/RelyingPartyRepository.ts @@ -0,0 +1,221 @@ +import { eq, inArray } from 'drizzle-orm' +import { BadRequestError, NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { NewRelyingParty, RelyingParty, RepositoryDefinition } from '../../types' +import { credentialDefinitions, relyingParties, relyingPartiesToCredentialDefinitions } from '../schema' +import AssetRepository from './AssetRepository' +import CredentialDefinitionRepository from './CredentialDefinitionRepository' + +@Service() +class RelyingPartyRepository implements RepositoryDefinition { + public constructor( + private readonly databaseService: DatabaseService, + private readonly credentialDefinitionRepository: CredentialDefinitionRepository, + private readonly assetRepository: AssetRepository, + ) {} + + public async create(relyingParty: NewRelyingParty): Promise { + if (relyingParty.credentialDefinitions && relyingParty.credentialDefinitions.length > 0) { + const credentialDefinitionPromises = relyingParty.credentialDefinitions.map( + async (credentialDefinition) => await this.credentialDefinitionRepository.findById(credentialDefinition), + ) + await Promise.all(credentialDefinitionPromises) + } + const logoResult = relyingParty.logo ? await this.assetRepository.findById(relyingParty.logo) : null + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [relyingPartyResult] = await tx + .insert(relyingParties) + .values({ + ...relyingParty, + logo: logoResult ? logoResult.id : null, + }) + .returning() + + let credentialDefinitionsResult: any[] = [] + + // Only insert if arrays are not empty + if (relyingParty.credentialDefinitions && relyingParty.credentialDefinitions.length > 0) { + const relyingPartiesToCredentialDefinitionsResult = await tx + .insert(relyingPartiesToCredentialDefinitions) + .values( + relyingParty.credentialDefinitions.map((credentialDefinitionId: string) => ({ + relyingParty: relyingPartyResult.id, + credentialDefinition: credentialDefinitionId, + })), + ) + .returning() + + credentialDefinitionsResult = await tx.query.credentialDefinitions.findMany({ + where: inArray( + credentialDefinitions.id, + relyingPartiesToCredentialDefinitionsResult.map((item) => item.credentialDefinition), + ), + with: { + cs: { with: { attributes: true } }, + representations: true, + revocation: true, + icon: true, + }, + }) + } + + return { + ...relyingPartyResult, + logo: logoResult, + credentialDefinitions: credentialDefinitionsResult.map((item: any) => ({ + ...item, + credentialSchema: item.cs, + })), + } + }) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(relyingParties).where(eq(relyingParties.id, id)) + } + + public async update(id: string, relyingParty: NewRelyingParty): Promise { + await this.findById(id) + + if (!relyingParty.credentialDefinitions || relyingParty.credentialDefinitions.length === 0) { + return Promise.reject(new BadRequestError('At least one credential definition is required')) + } + + const credentialDefinitionPromises = relyingParty.credentialDefinitions.map( + async (credentialDefinition) => await this.credentialDefinitionRepository.findById(credentialDefinition), + ) + await Promise.all(credentialDefinitionPromises) + const logoResult = relyingParty.logo ? await this.assetRepository.findById(relyingParty.logo) : null + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [relyingPartyResult] = await tx + .update(relyingParties) + .set({ + ...relyingParty, + logo: logoResult ? logoResult.id : null, + }) + .where(eq(relyingParties.id, id)) + .returning() + + await tx + .delete(relyingPartiesToCredentialDefinitions) + .where(eq(relyingPartiesToCredentialDefinitions.relyingParty, id)) + + const relyingPartiesToCredentialDefinitionsResult = await tx + .insert(relyingPartiesToCredentialDefinitions) + .values( + relyingParty.credentialDefinitions.map((credentialDefinitionId: string) => ({ + relyingParty: relyingPartyResult.id, + credentialDefinition: credentialDefinitionId, + })), + ) + .returning() + + const credentialDefinitionsResult = await tx.query.credentialDefinitions.findMany({ + where: inArray( + credentialDefinitions.id, + relyingPartiesToCredentialDefinitionsResult.map((item) => item.credentialDefinition), + ), + with: { + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + icon: true, + }, + }) + + return { + ...relyingPartyResult, + logo: logoResult, + credentialDefinitions: credentialDefinitionsResult.map((item: any) => ({ + ...item, + credentialSchema: item.cs, + })), + } + }) + } + + public async findById(id: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.relyingParties.findFirst({ + where: eq(relyingParties.id, id), + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + tenant: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No relying party found for id: ${id}`)) + } + + return { + ...result, + credentialDefinitions: result.cds.map((item: any) => ({ + ...item.cd, + credentialSchema: item.cd.cs, + })), + } + } + + public async findAll(): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.relyingParties.findMany({ + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + }, + }) + + return result.map((relyingParty) => ({ + ...relyingParty, + credentialDefinitions: relyingParty.cds.map((item: any) => ({ + ...item.cd, + credentialSchema: item.cd.cs, + })), + })) + } +} + +export default RelyingPartyRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/ScenarioRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/ScenarioRepository.ts new file mode 100644 index 000000000..c86bbf5a4 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/ScenarioRepository.ts @@ -0,0 +1,869 @@ +import { and, eq, inArray } from 'drizzle-orm' +import { BadRequestError } from 'routing-controllers' +import { NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { + AcceptCredentialAction, + AriesOOBAction, + AriesProofRequest, + ButtonAction, + ChooseWalletAction, + Issuer, + NewIssuanceScenario, + NewPresentationScenario, + NewScenario, + NewStep, + NewStepActionTypes, + RelyingParty, + RepositoryDefinition, + Scenario, + ScenarioFindAllArgs, + ScenarioType, + SetupConnectionAction, + Step, + StepAction, + StepActionType, + StepActionTypes, + Tx, +} from '../../types' +import { isIssuanceScenario, isPresentationScenario } from '../../utils/mappers' +import { generateSlug } from '../../utils/slug' +import { sortSteps } from '../../utils/sort' +import { + ariesProofRequests, + assets, + credentialDefinitions, + scenarios, + scenariosToPersonas, + stepActions, + steps, +} from '../schema' +import AssetRepository from './AssetRepository' +import IssuerRepository from './IssuerRepository' +import PersonaRepository from './PersonaRepository' +import RelyingPartyRepository from './RelyingPartyRepository' + +@Service() +class ScenarioRepository implements RepositoryDefinition { + public constructor( + private readonly databaseService: DatabaseService, + private readonly personaRepository: PersonaRepository, + private readonly issuerRepository: IssuerRepository, + private readonly relyingPartyRepository: RelyingPartyRepository, + private readonly assetRepository: AssetRepository, + ) {} + + private mapStepAction(action: StepAction & { proofRequest?: AriesProofRequest | null }): StepActionTypes { + switch (action.actionType) { + case StepActionType.ARIES_OOB: + return { + ...action, + actionType: StepActionType.ARIES_OOB, + proofRequest: action.proofRequest, + credentialDefinitionId: action.credentialDefinitionId, + } as AriesOOBAction + case StepActionType.ACCEPT_CREDENTIAL: + return { + ...action, + actionType: StepActionType.ACCEPT_CREDENTIAL, + credentialDefinitionId: action.credentialDefinitionId, + } as AcceptCredentialAction + case StepActionType.BUTTON: + return { + ...action, + actionType: StepActionType.BUTTON, + goToStep: action.goToStep || null, + } as ButtonAction + case StepActionType.SETUP_CONNECTION: + return { + ...action, + actionType: StepActionType.SETUP_CONNECTION, + } as SetupConnectionAction + case StepActionType.CHOOSE_WALLET: + return { + ...action, + actionType: StepActionType.CHOOSE_WALLET, + } as ChooseWalletAction + default: + return action as StepActionTypes + } + } + + // Returns an array of proof-request inserts based on the steps and the inserted stepActions. + private getAriesProofRequestInserts(stepsArray: NewStep[], insertedActions: any[]): any[] { + let offset = 0 + const inserts: any[] = [] + for (const step of stepsArray) { + step?.actions?.forEach((action, i) => { + if (action.actionType === StepActionType.ARIES_OOB && 'proofRequest' in action && action.proofRequest) { + inserts.push({ + ...action.proofRequest, + stepAction: insertedActions[offset + i].id, + }) + } + }) + offset += step?.actions?.length ?? 0 + } + return inserts + } + + // Inserts proof-requests for the provided steps and returns a map from stepAction id to proofRequest. + private async insertAriesProofRequestsForSteps( + tx: any, + stepsArray: NewStep[], + insertedActions: any[], + ): Promise> { + const inserts = this.getAriesProofRequestInserts(stepsArray, insertedActions) + const proofRequestsMap = new Map() + if (inserts.length > 0) { + const proofRequestsResult = await tx.insert(ariesProofRequests).values(inserts).returning() + proofRequestsResult.forEach((pr: any) => proofRequestsMap.set(pr.stepAction, pr)) + } + return proofRequestsMap + } + + public async create(scenario: NewScenario): Promise { + if (scenario.steps.length === 0) { + return Promise.reject(new BadRequestError('At least one step is required')) + } + if (scenario.personas.length === 0) { + return Promise.reject(new BadRequestError('At least one persona is required')) + } + + const bannerImageResult = scenario.bannerImage ? await this.assetRepository.findById(scenario.bannerImage) : null + + await Promise.all(scenario.personas.map((persona) => this.personaRepository.findById(persona))) + + const updatedSteps = await Promise.all( + scenario.steps.map(async (step: NewStep) => ({ + ...step, + })), + ) + + const scenarioType = isIssuanceScenario(scenario) ? ScenarioType.ISSUANCE : ScenarioType.PRESENTATION + + const scenarioPartyResult: Issuer | RelyingParty = isIssuanceScenario(scenario) + ? await this.issuerRepository.findById((scenario).issuer) + : await this.relyingPartyRepository.findById((scenario).relyingParty) + + const connection = await this.databaseService.getConnection() + const slug = await generateSlug({ + value: scenario.name, + connection, + schema: scenarios, + }) + + return connection.transaction(async (tx): Promise => { + const [scenarioResult] = await tx + .insert(scenarios) + .values({ + ...scenario, + slug, + ...(isIssuanceScenario(scenario) && { issuer: scenarioPartyResult.id }), + ...(isPresentationScenario(scenario) && { relyingParty: scenarioPartyResult.id }), + scenarioType: scenarioType, + }) + .returning() + + await tx + .insert(scenariosToPersonas) + .values( + scenario.personas.map((personaId: string) => ({ + scenario: scenarioResult.id, + persona: personaId, + })), + ) + .returning() + + const personasResult = await tx.query.personas.findMany({ + where: inArray( + credentialDefinitions.id, + (scenario.personas as (string | null)[]).filter((a): a is string => a !== null), + ), + with: { + headshotImage: true, + bodyImage: true, + }, + }) + + const stepsResult = await tx + .insert(steps) + .values( + updatedSteps.map((step: NewStep) => ({ + ...step, + scenario: scenarioResult.id, + })), + ) + .returning() + + const flatActions = stepsResult + .flatMap( + (stepResult, index) => + scenario.steps[index].actions && + scenario.steps[index].actions.map((action) => ({ + ...action, + step: stepResult.id, + })), + ) + .filter((actions) => !!actions) + + const stepActionsResult = await tx.insert(stepActions).values(flatActions).returning() + const proofRequestsMap = await this.insertAriesProofRequestsForSteps(tx, scenario.steps, stepActionsResult) + + const scenarioSteps = await Promise.all( + stepsResult.map(async (stepResult) => ({ + ...stepResult, + actions: stepActionsResult + .filter((sa: any) => sa.step === stepResult.id) + .map((action: any) => + this.mapStepAction({ + ...action, + proofRequest: proofRequestsMap.get(action.id) || null, + }), + ), + asset: ( + await tx.query.assets.findMany({ + where: inArray( + assets.id, + ([stepResult.asset] as (string | null)[]).filter((a): a is string => a !== null), + ), + }) + )[0], + })), + ) + + return { + id: scenarioResult.id, + name: scenarioResult.name, + slug: scenarioResult.slug, + description: scenarioResult.description, + steps: sortSteps(scenarioSteps as Step[]), + scenarioType: scenarioType, + ...(isIssuanceScenario(scenario) && { + issuer: scenarioPartyResult, + }), + ...(isPresentationScenario(scenario) && { + relyingParty: scenarioPartyResult, + }), + personas: personasResult, + createdAt: scenarioResult.createdAt, + updatedAt: scenarioResult.updatedAt, + hidden: scenarioResult.hidden, + bannerImage: bannerImageResult, + } + }) + } + + public async delete(scenarioId: string): Promise { + await this.findById(scenarioId) + await (await this.databaseService.getConnection()).delete(scenarios).where(eq(scenarios.id, scenarioId)) + } + + public async update(scenarioId: string, scenario: NewScenario): Promise { + if (scenario.steps.length === 0) { + return Promise.reject(new BadRequestError('At least one step is required')) + } + if (scenario.personas.length === 0) { + return Promise.reject(new BadRequestError('At least one persona is required')) + } + + const bannerImageResult = scenario.bannerImage ? await this.assetRepository.findById(scenario.bannerImage) : null + + await Promise.all(scenario.personas.map((persona) => this.personaRepository.findById(persona))) + + const updatedSteps = await Promise.all( + scenario.steps.map(async (step: NewStep) => ({ + ...step, + })), + ) + + const scenarioType = isIssuanceScenario(scenario) ? ScenarioType.ISSUANCE : ScenarioType.PRESENTATION + + const scenarioPartyResult: Issuer | RelyingParty = isIssuanceScenario(scenario) + ? await this.issuerRepository.findById((scenario).issuer) + : await this.relyingPartyRepository.findById((scenario).relyingParty) + + const connection = await this.databaseService.getConnection() + const slug = await generateSlug({ + value: scenario.name, + id: scenarioId, + connection, + schema: scenarios, + }) + + return connection.transaction(async (tx): Promise => { + const [scenarioResult] = await tx + .update(scenarios) + .set({ + ...scenario, + slug, + ...(isIssuanceScenario(scenario) && { issuer: scenarioPartyResult.id }), + ...(isPresentationScenario(scenario) && { relyingParty: scenarioPartyResult.id }), + scenarioType: scenarioType, + }) + .where(eq(scenarios.id, scenarioId)) + .returning() + + await tx.delete(scenariosToPersonas).where(eq(scenariosToPersonas.scenario, scenarioId)) + + await tx + .insert(scenariosToPersonas) + .values( + scenario.personas.map((personaId: string) => ({ + scenario: scenarioResult.id, + persona: personaId, + })), + ) + .returning() + + const personasResult = await tx.query.personas.findMany({ + where: inArray( + credentialDefinitions.id, + (scenario.personas as (string | null)[]).filter((a): a is string => a !== null), + ), + with: { + headshotImage: true, + bodyImage: true, + }, + }) + + await tx.delete(steps).where(eq(steps.scenario, scenarioId)) + + const stepsResult = await tx + .insert(steps) + .values( + updatedSteps.map((step: NewStep) => ({ + ...step, + scenario: scenarioResult.id, + })), + ) + .returning() + + const flatActions = stepsResult + .flatMap( + (stepResult, index) => + scenario.steps[index].actions && + scenario.steps[index].actions.map((action) => ({ + ...action, + step: stepResult.id, + })), + ) + .filter((actions) => !!actions) + const stepActionsResult = await tx.insert(stepActions).values(flatActions).returning() + const proofRequestsMap = await this.insertAriesProofRequestsForSteps(tx, scenario.steps, stepActionsResult) + + const scenarioSteps = await Promise.all( + stepsResult.map(async (stepResult) => ({ + ...stepResult, + actions: stepActionsResult + .filter((sa: any) => sa.step === stepResult.id) + .map((action: any) => ({ + ...action, + proofRequest: proofRequestsMap.get(action.id) || null, + })), + asset: ( + await tx.query.assets.findMany({ + where: inArray( + assets.id, + ([stepResult.asset] as (string | null)[]).filter((a): a is string => a !== null), + ), + }) + )[0], + })), + ) + + return { + id: scenarioResult.id, + name: scenarioResult.name, + slug: scenarioResult.slug, // TODO + description: scenarioResult.description, + steps: sortSteps(scenarioSteps), + scenarioType: scenarioType, + ...(isIssuanceScenario(scenario) && { + issuer: scenarioPartyResult, + }), + ...(isPresentationScenario(scenario) && { + relyingParty: scenarioPartyResult, + }), + personas: personasResult, + createdAt: scenarioResult.createdAt, + updatedAt: scenarioResult.updatedAt, + hidden: scenarioResult.hidden, + bannerImage: bannerImageResult, + } + }) + } + + public async findById(scenarioId: string, tx?: Tx): Promise { + const result = await (tx ?? (await this.databaseService.getConnection())).query.scenarios.findFirst({ + where: and(eq(scenarios.id, scenarioId)), + with: { + steps: { + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + }, + }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + bannerImage: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No scenario found for id: ${scenarioId}`)) + } + + return { + ...result, + steps: sortSteps(result.steps as Step[]), + ...(result.issuer && { + issuer: { + ...(result.issuer as any), // TODO check this typing issue at a later point in time + credentialDefinitions: result.issuer!.cds.map((credentialDefinition: any) => credentialDefinition.cd), + credentialSchemas: result.issuer!.css.map((credentialSchema: any) => credentialSchema.cs), + }, + }), + ...(result.relyingParty && { + relyingParty: { + ...(result.relyingParty as any), // TODO check this typing issue at a later point in time + credentialDefinitions: result.relyingParty!.cds.map((credentialDefinition: any) => credentialDefinition.cd), + }, + }), + personas: result.personas.map((item: any) => item.persona), + } + } + + public async findAll(args: ScenarioFindAllArgs): Promise { + const { filter } = args + const result = await ( + await this.databaseService.getConnection() + ).query.scenarios.findMany({ + where: eq(scenarios.scenarioType, filter.scenarioType), + with: { + steps: { + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + }, + }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + bannerImage: true, + }, + }) + + return result.map((scenario: any) => ({ + ...scenario, + steps: sortSteps(scenario.steps), + ...(scenario.issuer && { + issuer: { + ...(scenario.issuer as any), // TODO check this typing issue at a later point in time + credentialDefinitions: scenario.issuer!.cds.map((credentialDefinition: any) => credentialDefinition.cd), + credentialSchemas: scenario.issuer!.css.map((credentialSchema: any) => credentialSchema.cs), + }, + }), + ...(scenario.relyingParty && { + relyingParty: { + ...(scenario.relyingParty as any), // TODO check this typing issue at a later point in time + credentialDefinitions: scenario.relyingParty!.cds.map((credentialDefinition: any) => credentialDefinition.cd), + }, + }), + personas: scenario.personas.map((item: any) => item.persona), // TODO check this typing issue at a later point in time + })) + } + + public async createStep(scenarioId: string, step: NewStep): Promise { + await this.findById(scenarioId) + + const assetResult = step.asset ? await this.assetRepository.findById(step.asset) : null + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [stepResult] = await tx + .insert(steps) + .values({ + ...step, + scenario: scenarioId, + }) + .returning() + + if (step.actions && step.actions.length > 0) { + const actionsResult = await tx + .insert(stepActions) + .values( + step.actions.map((action) => ({ + ...action, + step: stepResult.id, + })), + ) + .returning() + + const proofRequestsMap = await this.insertAriesProofRequestsForSteps(tx, [step], actionsResult) + return { + ...stepResult, + actions: actionsResult.map((action) => ({ + ...action, + proofRequest: proofRequestsMap.get(action.id) || null, + })) as AriesOOBAction[], + asset: assetResult, + } + } + return { + ...stepResult, + asset: assetResult, + } + }) + } + + public async deleteStep(scenarioId: string, stepId: string): Promise { + await this.findByStepId(scenarioId, stepId) + await (await this.databaseService.getConnection()) + .delete(steps) + .where(and(eq(steps.id, stepId), eq(steps.scenario, scenarioId))) + } + + public async updateStep(scenarioId: string, stepId: string, step: NewStep): Promise { + await this.findById(scenarioId) + + const assetResult = step.asset ? await this.assetRepository.findById(step.asset) : null + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [stepResult] = await tx + .update(steps) + .set({ + ...step, + scenario: scenarioId, + }) + .where(eq(steps.id, stepId)) + .returning() + + await tx.delete(stepActions).where(eq(stepActions.step, stepId)) + + if (step.actions && step.actions.length > 0) { + const actionsResult = await tx + .insert(stepActions) + .values( + step.actions.map((action) => ({ + ...action, + step: stepResult.id, + })), + ) + .returning() + + const proofRequestsMap = await this.insertAriesProofRequestsForSteps(tx, [step], actionsResult) + return { + ...stepResult, + actions: actionsResult.map((action) => + this.mapStepAction({ + ...action, + proofRequest: proofRequestsMap.get(action.id) || null, + }), + ) as AriesOOBAction[], + asset: assetResult, + } + } + return { + ...stepResult, + asset: assetResult, + } + }) + } + + public async findByStepId(scenarioId: string, stepId: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.steps.findFirst({ + where: and(and(eq(steps.id, stepId), eq(steps.scenario, scenarioId))), + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No step found for scenario id: ${scenarioId} and step id: ${stepId}`)) + } + return { + ...result, + actions: result.actions.map((action) => this.mapStepAction(action)) as AriesOOBAction[], + } + } + + public async findAllSteps(scenarioId: string): Promise { + await this.findById(scenarioId) + const result = await ( + await this.databaseService.getConnection() + ).query.steps.findMany({ + where: eq(steps.scenario, scenarioId), + with: { + asset: true, + actions: { + with: { + proofRequest: true, + }, + }, + }, + }) + + return sortSteps(result as Step[]) + } + + public async createStepAction( + scenarioId: string, + stepId: string, + action: NewStepActionTypes, + ): Promise { + await this.findByStepId(scenarioId, stepId) + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [actionResult] = await tx + .insert(stepActions) + .values({ + ...action, + step: stepId, + }) + .returning() + + let proofRequestsResult = null + if (action.actionType === StepActionType.ARIES_OOB && 'proofRequest' in action) { + const [result] = await tx + .insert(ariesProofRequests) + .values({ + ...action.proofRequest, + stepAction: actionResult.id, + }) + .returning() + proofRequestsResult = result + } + + return this.mapStepAction({ + ...actionResult, + proofRequest: proofRequestsResult, + }) + }) + } + + public async deleteStepAction(scenarioId: string, stepId: string, actionId: string): Promise { + await this.findByStepActionId(scenarioId, stepId, actionId) + await (await this.databaseService.getConnection()) + .delete(stepActions) + .where(and(eq(stepActions.id, actionId), eq(stepActions.step, stepId))) + } + + public async updateStepAction( + scenarioId: string, + stepId: string, + actionId: string, + action: NewStepActionTypes, + ): Promise { + await this.findByStepId(scenarioId, stepId) + + return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { + const [actionResult] = await tx + .update(stepActions) + .set({ + ...action, + step: stepId, + }) + .where(eq(stepActions.id, actionId)) + .returning() + + let proofRequestsResult = null + if (action.actionType === StepActionType.ARIES_OOB && 'proofRequest' in action) { + await tx.delete(ariesProofRequests).where(eq(ariesProofRequests.stepAction, actionId)) + + const [result] = await tx + .insert(ariesProofRequests) + .values({ + ...action.proofRequest, + stepAction: actionResult.id, + }) + .returning() + proofRequestsResult = result + } + + return this.mapStepAction({ + ...actionResult, + proofRequest: proofRequestsResult, + }) + }) + } + + public async findByStepActionId(scenarioId: string, stepId: string, actionId: string): Promise { + await this.findByStepId(scenarioId, stepId) + const result = await ( + await this.databaseService.getConnection() + ).query.stepActions.findFirst({ + where: and(eq(stepActions.id, actionId)), + with: { + proofRequest: true, + }, + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No action found for step id ${stepId} and action id: ${actionId}`)) + } + + return this.mapStepAction(result) + } + + public async findAllStepActions(scenarioId: string, stepId: string): Promise { + await this.findByStepId(scenarioId, stepId) + const results = await ( + await this.databaseService.getConnection() + ).query.stepActions.findMany({ + where: and(eq(stepActions.step, stepId)), + with: { + proofRequest: true, + }, + }) + + return results.map((result) => this.mapStepAction(result)) + } + + public async findIdBySlug(slug: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.scenarios.findFirst({ + where: eq(scenarios.slug, slug), + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No scenario found for slug: ${slug}`)) + } + + return result.id + } +} + +export default ScenarioRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/ShowcaseRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/ShowcaseRepository.ts new file mode 100644 index 000000000..9d55bbf9b --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/ShowcaseRepository.ts @@ -0,0 +1,966 @@ +import { and, eq, inArray, isNull } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { NewShowcase, Persona, Scenario, Showcase, Step, TenantScopedRepositoryDefinition, Tx } from '../../types' +import { generateSlug } from '../../utils/slug' +import { sortSteps } from '../../utils/sort' +import { + personas, + scenarios, + showcases, + showcasesToCredentialDefinitions, + showcasesToPersonas, + showcasesToScenarios, +} from '../schema' +import AssetRepository from './AssetRepository' +import PersonaRepository from './PersonaRepository' +import ScenarioRepository from './ScenarioRepository' +import UserRepository from './UserRepository' + +@Service() +class ShowcaseRepository implements TenantScopedRepositoryDefinition { + public constructor( + private readonly databaseService: DatabaseService, + private readonly personaRepository: PersonaRepository, + private readonly scenarioRepository: ScenarioRepository, + private readonly assetRepository: AssetRepository, + private readonly userRepository: UserRepository, + ) {} + + public async create(showcase: NewShowcase): Promise { + let scenariosResult: Scenario[] = [] + let personasResult: Persona[] = [] + + const createdByResult = showcase?.createdBy ? await this.userRepository.findById(showcase.createdBy) : null + const approvedByResult = showcase?.approvedBy ? await this.userRepository.findById(showcase.approvedBy) : null + const bannerImageResult = showcase.bannerImage ? await this.assetRepository.findById(showcase.bannerImage) : null + + const connection = await this.databaseService.getConnection() + const slug = await generateSlug({ + value: showcase.name, + connection, + schema: showcases, + }) + + return connection.transaction(async (tx: Tx): Promise => { + const [showcaseResult] = await tx + .insert(showcases) + .values({ + ...showcase, + slug, + }) + .returning() + + if (showcase.scenarios.length > 0) { + const scenarioPromises = showcase.scenarios.map(async (scenario) => + this.scenarioRepository.findById(scenario, tx), + ) + await Promise.all(scenarioPromises) + + const showcasesToScenariosResult = await tx + .insert(showcasesToScenarios) + .values( + showcase.scenarios.map((scenarioId: string) => ({ + showcase: showcaseResult.id, + scenario: scenarioId, + })), + ) + .returning() + + scenariosResult = await tx.query.scenarios + .findMany({ + where: inArray( + scenarios.id, + showcasesToScenariosResult.map((item) => item.scenario).filter((id): id is string => id !== null), + ), + with: { + steps: { + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + }, + }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + bannerImage: true, + }, + }) + .then((scenarios) => + scenarios.map((scenario) => ({ + id: scenario.id, + name: scenario.name, + slug: scenario.slug, + description: scenario.description, + scenarioType: scenario.scenarioType, + hidden: scenario.hidden, + createdAt: scenario.createdAt, + updatedAt: scenario.updatedAt, + bannerImage: scenario.bannerImage, + steps: sortSteps(scenario.steps as Step[]), + personas: scenario.personas.map((p) => p.persona), + relyingParty: scenario.relyingParty + ? { + id: scenario.relyingParty.id, + name: scenario.relyingParty.name, + type: scenario.relyingParty.type, + description: scenario.relyingParty.description, + organization: scenario.relyingParty.organization, + tenantId: scenario.relyingParty.tenantId, + logo: scenario.relyingParty.logo, + credentialDefinitions: scenario.relyingParty.cds.map((cd: any) => ({ + ...cd.cd, + icon: cd.cd.icon || undefined, + credentialSchema: cd.cd.cs, + })), + createdAt: scenario.relyingParty.createdAt, + updatedAt: scenario.relyingParty.updatedAt, + } + : undefined, + issuer: scenario.issuer + ? { + id: scenario.issuer.id, + name: scenario.issuer.name, + type: scenario.issuer.type, + description: scenario.issuer.description, + organization: scenario.issuer.organization, + tenantId: scenario.issuer.tenantId, + logo: scenario.issuer.logo, + credentialDefinitions: scenario.issuer.cds.map((cd: any) => ({ + ...cd.cd, + icon: cd.cd.icon || undefined, + credentialSchema: cd.cd.cs, + })), + credentialSchemas: scenario.issuer.css.map((cs: any) => cs.cs), + createdAt: scenario.issuer.createdAt, + updatedAt: scenario.issuer.updatedAt, + } + : undefined, + })), + ) + } + + if (showcase.personas.length > 0) { + const personaPromises = showcase.personas.map( + async (persona) => await this.personaRepository.findById(persona, tx), + ) + await Promise.all(personaPromises) + + const showcasesToPersonasResult = await tx + .insert(showcasesToPersonas) + .values( + showcase.personas.map((personaId: string) => ({ + showcase: showcaseResult.id, + persona: personaId, + })), + ) + .returning() + + personasResult = await tx.query.personas.findMany({ + where: inArray( + personas.id, + showcasesToPersonasResult.map((item) => item.persona).filter((id): id is string => id !== null), + ), + with: { + headshotImage: true, + bodyImage: true, + }, + }) + } + + return { + ...(showcaseResult as any), // TODO check this typing issue at a later point in time + scenarios: scenariosResult, + personas: personasResult, + bannerImage: bannerImageResult, + createdBy: createdByResult, + approvedBy: approvedByResult, + } + }) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(showcases).where(eq(showcases.id, id)) + } + + public async update(id: string, showcase: NewShowcase): Promise { + const existingShowcase = await this.findById(id) + + if (existingShowcase.approvedBy) { + throw new Error('Showcase is already approved and cannot be updated') + } + + let scenariosResult: Scenario[] = [] + let personasResult: Persona[] = [] + let slug: string | undefined + + const createdByResult = showcase?.createdBy ? await this.userRepository.findById(showcase.createdBy) : null + const approvedByResult = showcase?.approvedBy ? await this.userRepository.findById(showcase.approvedBy) : null + const bannerImageResult = showcase.bannerImage ? await this.assetRepository.findById(showcase.bannerImage) : null + + const connection = await this.databaseService.getConnection() + + if (showcase.name !== (await this.findById(id)).name) { + slug = await generateSlug({ + value: showcase.name, + id, + connection, + schema: showcases, + }) + } else { + slug = (await this.findById(id)).slug + } + + return connection.transaction(async (tx): Promise => { + const [showcaseResult] = await tx + .update(showcases) + .set({ + ...showcase, + slug, + }) + .where(eq(showcases.id, id)) + .returning() + + await tx.delete(showcasesToPersonas).where(eq(showcasesToPersonas.showcase, id)) + await tx.delete(showcasesToScenarios).where(eq(showcasesToScenarios.showcase, id)) + + if (showcase.scenarios.length > 0) { + const scenarioPromises = showcase.scenarios.map(async (scenario) => + this.scenarioRepository.findById(scenario, tx), + ) + await Promise.all(scenarioPromises) + + const showcasesToScenariosResult = + (await tx + .insert(showcasesToScenarios) + .values( + showcase.scenarios.map((scenarioId: string) => ({ + showcase: showcaseResult.id, + scenario: scenarioId, + })), + ) + .returning()) || [] + + scenariosResult = await tx.query.scenarios + .findMany({ + where: inArray( + scenarios.id, + showcasesToScenariosResult.map((item) => item.scenario).filter((id): id is string => id !== null), + ), + with: { + steps: { + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + }, + }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + bannerImage: true, + }, + }) + .then((scenarios) => + scenarios.map((scenario) => ({ + id: scenario.id, + name: scenario.name, + slug: scenario.slug, + description: scenario.description, + scenarioType: scenario.scenarioType, + hidden: scenario.hidden, + createdAt: scenario.createdAt, + updatedAt: scenario.updatedAt, + bannerImage: scenario.bannerImage, + steps: sortSteps(scenario.steps as Step[]), + personas: scenario.personas.map((p) => p.persona), + relyingParty: scenario.relyingParty + ? { + id: scenario.relyingParty.id, + name: scenario.relyingParty.name, + type: scenario.relyingParty.type, + description: scenario.relyingParty.description, + organization: scenario.relyingParty.organization, + tenantId: scenario.relyingParty.tenantId, + logo: scenario.relyingParty.logo, + credentialDefinitions: scenario.relyingParty.cds.map((cd: any) => ({ + ...cd.cd, + icon: cd.cd.icon || undefined, + credentialSchema: cd.cd.cs, + })), + createdAt: scenario.relyingParty.createdAt, + updatedAt: scenario.relyingParty.updatedAt, + } + : undefined, + issuer: scenario.issuer + ? { + id: scenario.issuer.id, + name: scenario.issuer.name, + type: scenario.issuer.type, + description: scenario.issuer.description, + organization: scenario.issuer.organization, + tenantId: scenario.issuer.tenantId, + logo: scenario.issuer.logo, + credentialDefinitions: scenario.issuer.cds.map((cd: any) => ({ + ...cd.cd, + icon: cd.cd.icon || undefined, + credentialSchema: cd.cd.cs, + })), + credentialSchemas: scenario.issuer.css.map((cs: any) => cs.cs), + createdAt: scenario.issuer.createdAt, + updatedAt: scenario.issuer.updatedAt, + } + : undefined, + })), + ) + } + + if (showcase.personas.length > 0) { + const personaPromises = showcase.personas.map( + async (persona) => await this.personaRepository.findById(persona, tx), + ) + await Promise.all(personaPromises) + + const showcasesToPersonasResult = await tx + .insert(showcasesToPersonas) + .values( + showcase.personas.map((personaId: string) => ({ + showcase: showcaseResult.id, + persona: personaId, + })), + ) + .returning() + + personasResult = await tx.query.personas.findMany({ + where: inArray( + personas.id, + showcasesToPersonasResult.map((item) => item.persona).filter((id): id is string => id !== null), + ), + with: { + headshotImage: true, + bodyImage: true, + }, + }) + } + + return { + ...(showcaseResult as any), // TODO check this typing issue at a later point in time + scenarios: scenariosResult, + personas: personasResult, + bannerImage: bannerImageResult, + createdBy: createdByResult, + approvedBy: approvedByResult, + } + }) + } + + public async findById(id: string, tenantId?: string, tx?: Tx): Promise { + const findShowcaseById = `find_showcase_by_id_${id}` + const whereCondition = tenantId ? and(eq(showcases.id, id), eq(showcases.tenantId, tenantId)) : eq(showcases.id, id) + + const prepared = (tx ?? (await this.databaseService.getConnection())).query.showcases + .findFirst({ + where: whereCondition, + with: { + scenarios: { + with: { + scenario: { + with: { + steps: { + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + }, + }, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + bannerImage: true, + createdBy: true, + approver: true, + }, + }) + .prepare(findShowcaseById) + + const result = await prepared.execute() + + if (!result) { + return Promise.reject(new NotFoundError(`No showcase found for id: ${id}`)) + } + + return { + ...result, + scenarios: result.scenarios.map((scenario: any) => ({ + ...scenario.scenario, + steps: sortSteps(scenario.scenario.steps), + ...(scenario.scenario.relyingParty && { + relyingParty: { + id: scenario.scenario.relyingParty.id, + name: scenario.scenario.relyingParty.name, + type: scenario.scenario.relyingParty.type, + description: scenario.scenario.relyingParty.description, + organization: scenario.scenario.relyingParty.organization, + tenantId: scenario.scenario.relyingParty.tenantId, + logo: scenario.scenario.relyingParty.logo, + credentialDefinitions: scenario.scenario.relyingParty.cds.map((cd: any) => cd.cd), + createdAt: scenario.scenario.relyingParty.createdAt, + updatedAt: scenario.scenario.relyingParty.updatedAt, + }, + }), + ...(scenario.scenario.issuer && { + issuer: { + id: scenario.scenario.issuer.id, + name: scenario.scenario.issuer.name, + type: scenario.scenario.issuer.type, + description: scenario.scenario.issuer.description, + organization: scenario.scenario.issuer.organization, + tenantId: scenario.scenario.issuer.tenantId, + logo: scenario.scenario.issuer.logo, + credentialDefinitions: scenario.scenario.issuer.cds.map((cd: any) => cd.cd), + credentialSchemas: scenario.scenario.issuer.css.map((cs: any) => cs.cs), + createdAt: scenario.scenario.issuer.createdAt, + updatedAt: scenario.scenario.issuer.updatedAt, + }, + }), + personas: scenario.scenario.personas.map((item: any) => item.persona), + })), + personas: result.personas.map((item: any) => item.persona), + approvedBy: result.approver, + } + } + + public async findAll(tenantId: string): Promise { + const connection = await this.databaseService.getConnection() + const showcasesResult = await connection.query.showcases.findMany({ + where: eq(showcases.tenantId, tenantId), + with: { bannerImage: true }, + }) + const showcaseIds = showcasesResult.map((s: any) => s.id) + + const [credDefData, scenariosData, personasData] = await Promise.all([ + connection.query.showcasesToCredentialDefinitions.findMany({ + where: inArray(showcasesToCredentialDefinitions.showcase, showcaseIds), + with: { + credentialDefinition: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }, + }, + }), + connection.query.showcasesToScenarios.findMany({ + where: inArray(showcasesToScenarios.showcase, showcaseIds), + with: { + scenario: { + with: { + steps: { + with: { + actions: { + with: { + proofRequest: true, + }, + }, + asset: true, + }, + }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }, + }, + }, + css: { + with: { + cs: { + with: { + attributes: true, + }, + }, + }, + }, + logo: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { + with: { + attributes: true, + }, + }, + representations: true, + revocation: true, + approver: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }, + }, + }, + }, + }), + connection.query.showcasesToPersonas.findMany({ + where: inArray(showcasesToPersonas.showcase, showcaseIds), + with: { + persona: { + with: { + headshotImage: true, + bodyImage: true, + }, + }, + }, + }), + ]) + + // Group join records by showcase id + const credDefMap = new Map() + for (const item of credDefData) { + const key = item.showcase + if (!credDefMap.has(key)) { + credDefMap.set(key, []) + } + credDefMap.get(key)!.push(item) + } + + const scenariosMap = new Map() + for (const item of scenariosData) { + const key = item.showcase + if (!scenariosMap.has(key)) { + scenariosMap.set(key, []) + } + scenariosMap.get(key)!.push(item) + } + + const personasMap = new Map() + for (const item of personasData) { + const key = item.showcase + if (!personasMap.has(key)) { + personasMap.set(key, []) + } + personasMap.get(key)!.push(item) + } + + return showcasesResult.map((showcase: any) => { + return { + ...showcase, + scenarios: (scenariosMap.get(showcase.id) || []).map((s: any) => { + const scenarioData = { ...s.scenario } + scenarioData.steps = sortSteps(s.scenario.steps) + if (s.scenario.relyingParty) { + scenarioData.relyingParty = { + id: s.scenario.relyingParty.id, + name: s.scenario.relyingParty.name, + type: s.scenario.relyingParty.type, + description: s.scenario.relyingParty.description, + organization: s.scenario.relyingParty.organization, + tenantId: s.scenario.relyingParty.tenantId, + logo: s.scenario.relyingParty.logo, + credentialDefinitions: s.scenario.relyingParty.cds.map((item: any) => item.cd), + createdAt: s.scenario.relyingParty.createdAt, + updatedAt: s.scenario.relyingParty.updatedAt, + } + } + if (s.scenario.issuer) { + scenarioData.issuer = { + id: s.scenario.issuer.id, + name: s.scenario.issuer.name, + type: s.scenario.issuer.type, + description: s.scenario.issuer.description, + organization: s.scenario.issuer.organization, + tenantId: s.scenario.issuer.tenantId, + logo: s.scenario.issuer.logo, + credentialDefinitions: s.scenario.issuer.cds.map((item: any) => item.cd), + credentialSchemas: s.scenario.issuer.css.map((item: any) => item.cs), + createdAt: s.scenario.issuer.createdAt, + updatedAt: s.scenario.issuer.updatedAt, + } + } + scenarioData.personas = s.scenario.personas.map((p: any) => p.persona) + return scenarioData + }), + credentialDefinitions: (credDefMap.get(showcase.id) || []).map((item: any) => ({ + ...item.credentialDefinition, + credentialSchema: item.credentialDefinition.cs, + })), + personas: (personasMap.get(showcase.id) || []).map((item: any) => item.persona), + } + }) + } + + public async findIdBySlug(slug: string, tenantId: string): Promise { + const result = await ( + await this.databaseService.getConnection() + ).query.showcases.findFirst({ + where: and(eq(showcases.slug, slug), eq(showcases.tenantId, tenantId)), + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No showcase found for slug: ${slug}`)) + } + + return result.id + } + + public async findUnapproved(tenantId: string): Promise { + const results = await ( + await this.databaseService.getConnection() + ).query.showcases.findMany({ + where: and(isNull(showcases.approvedAt), eq(showcases.tenantId, tenantId)), + with: { + scenarios: { + with: { + scenario: { + with: { + steps: { with: { actions: { with: { proofRequest: true } }, asset: true } }, + issuer: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { with: { attributes: true } }, + representations: true, + revocation: true, + approver: true, + }, + }, + }, + }, + css: { with: { cs: { with: { attributes: true } } } }, + logo: true, + }, + }, + relyingParty: { + with: { + cds: { + with: { + cd: { + with: { + icon: true, + cs: { with: { attributes: true } }, + representations: true, + revocation: true, + approver: true, + }, + }, + }, + }, + logo: true, + }, + }, + personas: { with: { persona: { with: { headshotImage: true, bodyImage: true } } } }, + }, + }, + }, + }, + personas: { with: { persona: { with: { headshotImage: true, bodyImage: true } } } }, + bannerImage: true, + createdBy: true, + approver: true, + }, + }) + + // Map results using the same logic as findById + return results.map((result) => ({ + ...result, + scenarios: (result.scenarios || []).map((scenario: any) => ({ + ...scenario.scenario, + steps: sortSteps(scenario.scenario.steps), + ...(scenario.scenario.relyingParty && { + relyingParty: { + id: scenario.scenario.relyingParty.id, + name: scenario.scenario.relyingParty.name, + type: scenario.scenario.relyingParty.type, + description: scenario.scenario.relyingParty.description, + organization: scenario.scenario.relyingParty.organization, + tenantId: scenario.scenario.relyingParty.tenantId, + logo: scenario.scenario.relyingParty.logo, + credentialDefinitions: scenario.scenario.relyingParty.cds.map((cd: any) => cd.cd), + createdAt: scenario.scenario.relyingParty.createdAt, + updatedAt: scenario.scenario.relyingParty.updatedAt, + }, + }), + ...(scenario.scenario.issuer && { + issuer: { + id: scenario.scenario.issuer.id, + name: scenario.scenario.issuer.name, + type: scenario.scenario.issuer.type, + description: scenario.scenario.issuer.description, + organization: scenario.scenario.issuer.organization, + tenantId: scenario.scenario.issuer.tenantId, + logo: scenario.scenario.issuer.logo, + credentialDefinitions: scenario.scenario.issuer.cds.map((cd: any) => cd.cd), + credentialSchemas: scenario.scenario.issuer.css.map((cs: any) => cs.cs), + createdAt: scenario.scenario.issuer.createdAt, + updatedAt: scenario.scenario.issuer.updatedAt, + }, + }), + personas: scenario.scenario.personas.map((item: any) => item.persona), + })), + personas: result.personas.map((p: any) => p.persona), + bannerImage: result.bannerImage ?? undefined, + createdBy: result.createdBy ?? undefined, + approvedBy: result.approver, + })) + } + + public async approve(id: string, tenantId: string, userId: string): Promise { + const now = new Date() + const connection = await this.databaseService.getConnection() + return await connection.transaction(async (tx) => { + await tx + .update(showcases) + .set({ + approvedBy: userId, + approvedAt: now, + updatedAt: now, + }) + .where(and(eq(showcases.id, id), eq(showcases.tenantId, tenantId))) + return await this.findById(id, tenantId, tx) + }) + } +} + +export default ShowcaseRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/TenantRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/TenantRepository.ts new file mode 100644 index 000000000..79fc079a3 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/TenantRepository.ts @@ -0,0 +1,282 @@ +import { and, eq, inArray } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Inject, Service } from 'typedi' + +import DatabaseService from '../../services/DatabaseService' +import { NewTenant, RepositoryDefinition, Tenant } from '../../types' +import { tenants, tenantsToUsers } from '../schema' +import UserRepository from './UserRepository' + +@Service() +class TenantRepository implements RepositoryDefinition { + public constructor(private readonly databaseService: DatabaseService) {} + + // Problem with circular references: https://github.com/typestack/typedi/blob/develop/docs/README.md#problem-with-circular-references + @Inject((type) => UserRepository) + private userRepository!: UserRepository + + public async create(newTenant: NewTenant): Promise { + const connection = await this.databaseService.getConnection() + if (newTenant.users && newTenant.users.length > 0) { + await Promise.all(newTenant.users.map(async (userId) => this.userRepository.findById(userId))) + } + return connection.transaction( + async (tx): Promise => { + const [tenantResult] = await tx + .insert(tenants) + .values({ + ...newTenant, + }) + .returning() + + if (newTenant.users && newTenant.users.length > 0) { + await tx + .insert(tenantsToUsers) + .values( + newTenant.users.map((userId: string) => ({ + tenant: tenantResult.id, + user: userId, + })), + ) + .returning() + } + + const tenant = await tx.query.tenants.findFirst({ + where: eq(tenants.id, tenantResult.id), + with: { + users: { + with: { + user: true, + }, + }, + issuers: { + with: { + cds: true, + css: true, + }, + }, + relyingParties: { + with: { + cds: true, + }, + }, + }, + }) + + if (!tenant) { + return Promise.reject(new Error(`Failed to fetch created tenant: ${tenantResult.id}`)) + } + + return { + ...tenant, + users: tenant.users.map((item: any) => item.user), + issuers: tenant.issuers.map((issuer: any) => ({ + ...issuer, + logo: null, + credentialDefinitions: issuer.cds.map((item: any) => ({ id: item.credentialDefinition })), + credentialSchemas: issuer.css.map((item: any) => ({ id: item.credentialSchema })), + })), + relyingParties: tenant.relyingParties.map((rp: any) => ({ + ...rp, + logo: null, + credentialDefinitions: rp.cds.map((item: any) => ({ id: item.credentialDefinition })), + })), + } + }, + { + isolationLevel: 'read committed', + accessMode: 'read write', + deferrable: true, + }, + ) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(tenants).where(eq(tenants.id, id)) + } + + public async update(id: string, newTenant: NewTenant): Promise { + await this.findById(id) + + const connection = await this.databaseService.getConnection() + if (newTenant.users && newTenant.users.length > 0) { + await Promise.all(newTenant.users.map(async (user) => this.userRepository.findById(user))) + } + return connection.transaction(async (tx): Promise => { + const [tenantResult] = await tx.update(tenants).set(newTenant).where(eq(tenants.id, id)).returning() + + await tx.delete(tenantsToUsers).where(eq(tenantsToUsers.tenant, id)) + + if (newTenant.users && newTenant.users.length > 0) { + await tx + .insert(tenantsToUsers) + .values( + newTenant.users.map((userId: string) => ({ + tenant: tenantResult.id, + user: userId, + })), + ) + .returning() + } + + const tenant = await tx.query.tenants.findFirst({ + where: eq(tenants.id, tenantResult.id), + with: { + users: { + with: { + user: true, + }, + }, + issuers: { + with: { + cds: true, + css: true, + }, + }, + relyingParties: { + with: { + cds: true, + }, + }, + }, + }) + + if (!tenant) { + return Promise.reject(new Error(`Failed to fetch updated tenant: ${tenantResult.id}`)) + } + + return { + ...tenant, + users: tenant.users.map((item: any) => item.user), + issuers: tenant.issuers.map((issuer: any) => ({ + ...issuer, + logo: null, + credentialDefinitions: issuer.cds.map((item: any) => ({ id: item.credentialDefinition })), + credentialSchemas: issuer.css.map((item: any) => ({ id: item.credentialSchema })), + })), + relyingParties: tenant.relyingParties.map((rp: any) => ({ + ...rp, + logo: null, + credentialDefinitions: rp.cds.map((item: any) => ({ id: item.credentialDefinition })), + })), + } + }) + } + + public async findById(id: string): Promise { + const prepared = (await this.databaseService.getConnection()).query.tenants + .findFirst({ + where: eq(tenants.id, id), + with: { + users: true, + issuers: { + with: { + cds: true, + css: true, + }, + }, + relyingParties: { + with: { + cds: true, + }, + }, + }, + }) + .prepare('statement_name') + + const tenant = await prepared.execute() + + if (!tenant) { + return Promise.reject(new NotFoundError(`No tenant found for id: ${id}`)) + } + + return { + ...tenant, + users: tenant.users.map((item: any) => item.user), + issuers: tenant.issuers.map((issuer: any) => ({ + ...issuer, + logo: null, + credentialDefinitions: issuer.cds.map((item: any) => ({ id: item.credentialDefinition })), + credentialSchemas: issuer.css.map((item: any) => ({ id: item.credentialSchema })), + })), + relyingParties: tenant.relyingParties.map((rp: any) => ({ + ...rp, + logo: null, + credentialDefinitions: rp.cds.map((item: any) => ({ id: item.credentialDefinition })), + })), + } + } + + public async findByIssuerAndClientId(issuer: string, clientId: string): Promise { + const prepared = (await this.databaseService.getConnection()).query.tenants + .findFirst({ + where: and(eq(tenants.id, clientId), eq(tenants.oidcIssuer, issuer)), + }) + .prepare('find_tenant_by_issuer_and_clientId') + + const tenant = await prepared.execute() + + if (!tenant) { + return Promise.reject(new NotFoundError(`No tenant found for issuer: ${issuer} and clientId: ${clientId}`)) + } + return tenant + } + + public async findAll(): Promise { + const connection = await this.databaseService.getConnection() + const tenantsResult = await connection.query.tenants.findMany({ + with: { + issuers: { + with: { + cds: true, + css: true, + }, + }, + relyingParties: { + with: { + cds: true, + }, + }, + }, + }) + + const tenantIds = tenantsResult.map((s: any) => s.id) + + const users = await connection.query.tenantsToUsers.findMany({ + where: inArray(tenantsToUsers.tenant, tenantIds), + with: { + user: true, + }, + }) + + const usersMap = users.reduce((map, item) => { + const key = item.tenant + if (!map.has(key)) { + map.set(key, []) + } + map.get(key)!.push(item) + return map + }, new Map()) + + return tenantsResult.map((tenant) => { + return { + ...tenant, + users: (usersMap.get(tenant.id) || []).map((item: any) => item.user), + issuers: tenant.issuers.map((issuer: any) => ({ + ...issuer, + logo: null, + credentialDefinitions: issuer.cds.map((item: any) => ({ id: item.credentialDefinition })), + credentialSchemas: issuer.css.map((item: any) => ({ id: item.credentialSchema })), + })), + relyingParties: tenant.relyingParties.map((rp: any) => ({ + ...rp, + logo: null, + credentialDefinitions: rp.cds.map((item: any) => ({ id: item.credentialDefinition })), + })), + } + }) + } +} + +export default TenantRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/UserRepository.ts b/apps/bc-wallet-api-server/src/database/repositories/UserRepository.ts new file mode 100644 index 000000000..8b662194b --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/UserRepository.ts @@ -0,0 +1,173 @@ +import { eq, inArray } from 'drizzle-orm' +import { NotFoundError } from 'routing-controllers' +import { Inject, Service } from 'typedi' + +import { DatabaseService } from '../../services/DatabaseService' +import { NewUser, RepositoryDefinition, Tenant, User } from '../../types' +import { tenants, tenantsToUsers, users } from '../schema' +import TenantRepository from './TenantRepository' + +@Service() +export class UserRepository implements RepositoryDefinition { + public constructor(private readonly databaseService: DatabaseService) {} + + // Problem with circular references: https://github.com/typestack/typedi/blob/develop/docs/README.md#problem-with-circular-references + @Inject((type) => TenantRepository) + private tenantRepository!: TenantRepository + + public async create(newUser: NewUser): Promise { + let tenantsResult: Tenant[] = [] + const connection = await this.databaseService.getConnection() + if (newUser.tenants && newUser.tenants.length > 0) { + await Promise.all(newUser.tenants.map(async (tenantId) => this.tenantRepository.findById(tenantId))) + } + return connection.transaction(async (tx): Promise => { + const [userResult] = await tx.insert(users).values(newUser).returning() + + if (newUser.tenants && newUser.tenants.length > 0) { + const tenantsToUsersResult = await tx + .insert(tenantsToUsers) + .values( + newUser.tenants.map((tenantId: string) => ({ + user: userResult.id, + tenant: tenantId, + })), + ) + .returning() + + tenantsResult = await tx.query.tenants.findMany({ + where: inArray( + tenants.id, + tenantsToUsersResult.map((item) => item.tenant).filter((id): id is string => id !== null), + ), + }) + } + + return { + ...userResult, + tenants: tenantsResult, + } + }) + } + + public async delete(id: string): Promise { + await this.findById(id) + await (await this.databaseService.getConnection()).delete(users).where(eq(users.id, id)) + } + + public async update(id: string, newUser: NewUser): Promise { + await this.findById(id) + + let tenantsResult: Tenant[] = [] + const connection = await this.databaseService.getConnection() + if (newUser.tenants && newUser.tenants.length > 0) { + await Promise.all(newUser.tenants.map(async (tenantId) => this.tenantRepository.findById(tenantId))) + } + return connection.transaction(async (tx): Promise => { + const [userResult] = await tx.update(users).set(newUser).where(eq(users.id, id)).returning() + + await tx.delete(tenantsToUsers).where(eq(tenantsToUsers.user, id)) + + if (newUser.tenants && newUser.tenants.length > 0) { + const tenantsToUsersResult = await tx + .insert(tenantsToUsers) + .values( + newUser.tenants.map((tenantId: string) => ({ + user: userResult.id, + tenant: tenantId, + })), + ) + .returning() + + tenantsResult = await tx.query.tenants.findMany({ + where: inArray( + tenants.id, + tenantsToUsersResult.map((item) => item.tenant).filter((id): id is string => id !== null), + ), + }) + } + + return { + ...userResult, + tenants: tenantsResult, + } + }) + } + + public async findById(id: string): Promise { + const prepared = (await this.databaseService.getConnection()).query.users + .findFirst({ + where: eq(users.id, id), + with: { + tenants: true, + }, + }) + .prepare('statement_name') + + const user = await prepared.execute() + + if (!user) { + return Promise.reject(new NotFoundError(`No user found for id: ${id}`)) + } + + return { + ...user, + tenants: user.tenants.map((item: any) => item.tenant), + } + } + + public async findByUsernameAndTenantId(userName: string, tenantId: string): Promise { + const prepared = (await this.databaseService.getConnection()).query.users + .findFirst({ + where: eq(users.userName, userName), + with: { + tenants: { + where: eq(tenantsToUsers.tenant, tenantId), + }, + }, + }) + .prepare('find_user_by_username') + + const user = await prepared.execute() + + if (!user) { + return Promise.reject(new NotFoundError(`No user found for userName: ${userName}`)) + } + + return { + ...user, + tenants: user.tenants.map((item: any) => item.tenant), + } + } + + public async findAll(): Promise { + const connection = await this.databaseService.getConnection() + const usersList = await connection.query.users.findMany() + const userIds = usersList.map((u: any) => u.id) + + const tenants = await connection.query.tenantsToUsers.findMany({ + where: inArray(tenantsToUsers.user, userIds), + with: { + tenant: true, + }, + }) + + const tenantsMap = tenants.reduce((map, item) => { + const key = item.user + if (!map.has(key)) { + map.set(key, []) + } + map.get(key)!.push(item) + return map + }, new Map()) + + return usersList.map((user) => { + return { + ...user, + tenants: (tenantsMap.get(user.id) || []).map((item: any) => item.tenant), + } + }) + } +} + +export default UserRepository diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/asset.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/asset.repository.test.ts new file mode 100644 index 000000000..a3bdfd4b2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/asset.repository.test.ts @@ -0,0 +1,126 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { NewAsset } from '../../../types' +import * as schema from '../../schema' +import AssetRepository from '../AssetRepository' + +describe('Database asset repository tests', (): void => { + let client: PGlite + let repository: AssetRepository + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + repository = Container.get(AssetRepository) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save asset to database', async (): Promise => { + const asset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + + const savedAsset = await repository.create(asset) + + expect(savedAsset).toBeDefined() + expect(savedAsset.mediaType).toEqual(asset.mediaType) + expect(savedAsset.fileName).toEqual(asset.fileName) + expect(savedAsset.description).toEqual(asset.description) + expect(savedAsset.content).toStrictEqual(asset.content) + }) + + it('Should get asset by id from database', async (): Promise => { + const asset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + + const savedAsset = await repository.create(asset) + expect(savedAsset).toBeDefined() + + const fromDb = await repository.findById(savedAsset.id) + + expect(fromDb).toBeDefined() + expect(fromDb!.mediaType).toEqual(asset.mediaType) + expect(fromDb!.fileName).toEqual(asset.fileName) + expect(fromDb!.description).toEqual(asset.description) + expect(fromDb!.content).toStrictEqual(asset.content) + }) + + it('Should get all assets from database', async (): Promise => { + const asset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + + const savedAsset1 = await repository.create(asset) + expect(savedAsset1).toBeDefined() + + const savedAsset2 = await repository.create(asset) + expect(savedAsset2).toBeDefined() + + const fromDb = await repository.findAll() + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete asset from database', async (): Promise => { + const asset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + + const savedAsset = await repository.create(asset) + expect(savedAsset).toBeDefined() + + await repository.delete(savedAsset.id) + + await expect(repository.findById(savedAsset.id)).rejects.toThrowError(`No asset found for id: ${savedAsset.id}`) + }) + + it('Should update asset in database', async (): Promise => { + const asset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + + const savedAsset = await repository.create(asset) + expect(savedAsset).toBeDefined() + + const newFileName = 'new_image.png' + const updatedAsset = await repository.update(savedAsset.id, { ...savedAsset, fileName: newFileName }) + + expect(updatedAsset).toBeDefined() + expect(updatedAsset.mediaType).toEqual(asset.mediaType) + expect(updatedAsset.fileName).toEqual(newFileName) + expect(updatedAsset.description).toEqual(asset.description) + expect(updatedAsset.content).toStrictEqual(asset.content) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/credentialDefinition.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/credentialDefinition.repository.test.ts new file mode 100644 index 000000000..060ac8fb0 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/credentialDefinition.repository.test.ts @@ -0,0 +1,479 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { + Asset, + CredentialSchema, + CredentialType, + IdentifierType, + NewCredentialDefinition, + Tenant, + User, +} from '../../../types' +import * as schema from '../../schema' +import CredentialDefinitionRepository from '../CredentialDefinitionRepository' +import { createTestAsset, createTestCredentialSchema, createTestTenant, createTestUser } from './dbTestData' + +// Helper to check if a date is recent (within the last N seconds) +const isRecentDate = (date: Date | null | undefined, seconds = 5): boolean => { + if (!date) return false + const now = new Date() + const diff = now.getTime() - date.getTime() + return diff >= 0 && diff < seconds * 1000 +} + +describe('Database credential definition repository tests', (): void => { + let client: PGlite + let database: NodePgDatabase + let credentialDefinitionRepository: CredentialDefinitionRepository + let testUser: User, asset: Asset, credentialSchema: CredentialSchema, tenant: Tenant + + beforeEach(async (): Promise => { + client = new PGlite() + database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + + credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + + // Use helper functions from dbTestData to create test dependencies + testUser = await createTestUser('test-user') + asset = await createTestAsset() + tenant = await createTestTenant('test-tenant') + credentialSchema = await createTestCredentialSchema() + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save credential definition to database with null approval fields', async (): Promise => { + const credentialDefinition: NewCredentialDefinition = { + name: 'example_name', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + //revocation: { + // TODO SHOWCASE-80 AnonCredRevocation + //title: 'example_revocation_title', + // description: 'example_revocation_description', + // }, + } + + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + + expect(savedCredentialDefinition).toBeDefined() + expect(savedCredentialDefinition.name).toEqual(credentialDefinition.name) + expect(savedCredentialDefinition.version).toEqual(credentialDefinition.version) + expect(savedCredentialDefinition.icon).toBeDefined() + expect(savedCredentialDefinition.icon!.id).toEqual(asset.id) + expect(savedCredentialDefinition.tenantId).toEqual(tenant.id) + + // Verify approval fields are initially null/undefined + expect(savedCredentialDefinition.approvedBy).toBeNull() + expect(savedCredentialDefinition.approvedAt).toBeNull() + + // TODO SHOWCASE-81 representations + //expect(savedCredentialDefinition.representations.length).toEqual(2) + // TODO SHOWCASE-80 AnonCredRevocation + // expect(savedCredentialDefinition.revocation).not.toBeNull() + // expect(savedCredentialDefinition.revocation!.title).toEqual(credentialDefinition.revocation!.title) + // expect(savedCredentialDefinition.revocation!.description).toEqual(credentialDefinition.revocation!.description) + }) + + it('Should approve a credential definition', async (): Promise => { + const credentialDefinition: NewCredentialDefinition = { + name: 'to_approve', + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + //revocation: { + // TODO SHOWCASE-80 AnonCredRevocation + //title: 'example_revocation_title', + // description: 'example_revocation_description', + // }, + } + + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + expect(savedCredentialDefinition.approvedBy).toBeNull() + expect(savedCredentialDefinition.approvedAt).toBeNull() + + const approvedDefinition = await credentialDefinitionRepository.approve(savedCredentialDefinition.id, testUser.id) + + expect(approvedDefinition).toBeDefined() + expect(approvedDefinition.id).toEqual(savedCredentialDefinition.id) + expect(approvedDefinition.approvedBy).toBeDefined() + expect(approvedDefinition.approvedBy?.id).toEqual(testUser.id) + expect(approvedDefinition.approvedBy?.userName).toEqual(testUser.userName) + expect(approvedDefinition.approvedAt).toBeDefined() + expect(isRecentDate(approvedDefinition.approvedAt)).toBe(true) + + // Verify persistence + const fromDb = await credentialDefinitionRepository.findById(savedCredentialDefinition.id) + expect(fromDb.approvedBy?.id).toEqual(testUser.id) + expect(isRecentDate(fromDb.approvedAt)).toBe(true) + }) + + it('Should get credential definition by id including approval status', async (): Promise => { + // 1. Create and save + const credentialDefinition: NewCredentialDefinition = { + name: 'approved_def', + version: '1.1', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + //revocation: { + // TODO SHOWCASE-80 AnonCredRevocation + //title: 'example_revocation_title', + // description: 'example_revocation_description', + // }, + } + + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + + // 2. Approve it + await credentialDefinitionRepository.approve(savedCredentialDefinition.id, testUser.id) + + // 3. Fetch by ID + const fromDb = await credentialDefinitionRepository.findById(savedCredentialDefinition.id) + + // 4. Assert + expect(fromDb).toBeDefined() + expect(fromDb.name).toEqual(credentialDefinition.name) + expect(fromDb.icon).toBeDefined() + expect(fromDb.icon!.id).toEqual(asset.id) + expect(fromDb.tenantId).toEqual(tenant.id) + expect(fromDb.approvedBy).toBeDefined() + expect(fromDb.approvedBy?.id).toEqual(testUser.id) + expect(fromDb.approvedAt).toBeDefined() + expect(isRecentDate(fromDb.approvedAt)).toBe(true) + }) + + it('Should get all credential definitions including approval status', async (): Promise => { + // 1. Create unapproved + const cd1: NewCredentialDefinition = { + name: 'unapproved_cd', + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + const savedCd1 = await credentialDefinitionRepository.create(cd1) + + // 2. Create and approve + const cd2: NewCredentialDefinition = { + name: 'approved_cd', + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + let savedCd2 = await credentialDefinitionRepository.create(cd2) + savedCd2 = await credentialDefinitionRepository.approve(savedCd2.id, testUser.id) + + // 3. Fetch all + const fromDb = await credentialDefinitionRepository.findAll() + + // 4. Assert + expect(fromDb.length).toEqual(2) + + const fetchedCd1 = fromDb.find((cd) => cd.id === savedCd1.id) + const fetchedCd2 = fromDb.find((cd) => cd.id === savedCd2.id) + + expect(fetchedCd1).toBeDefined() + expect(fetchedCd1?.name).toEqual(cd1.name) + expect(fetchedCd1?.approvedBy).toBeNull() + expect(fetchedCd1?.approvedAt).toBeNull() + expect(fetchedCd1?.tenantId).toEqual(tenant.id) + + expect(fetchedCd2).toBeDefined() + expect(fetchedCd2?.name).toEqual(cd2.name) + expect(fetchedCd2?.approvedBy).toBeDefined() + expect(fetchedCd2?.approvedBy?.id).toEqual(testUser.id) + expect(fetchedCd2?.approvedAt).toBeDefined() + expect(isRecentDate(fetchedCd2?.approvedAt)).toBe(true) + expect(fetchedCd2?.tenantId).toEqual(tenant.id) + }) + + it('Should find only unapproved credential definitions', async (): Promise => { + // 1. Create unapproved + const cd1: NewCredentialDefinition = { + name: 'unapproved_cd_only', + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + const savedCd1 = await credentialDefinitionRepository.create(cd1) + + // 2. Create and approve + const cd2: NewCredentialDefinition = { + name: 'approved_cd_not_found', + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + let savedCd2 = await credentialDefinitionRepository.create(cd2) + await credentialDefinitionRepository.approve(savedCd2.id, testUser.id) + + // 3. Fetch unapproved + const unapproved = await credentialDefinitionRepository.findUnapproved() + + // 4. Assert + expect(unapproved.length).toEqual(1) + expect(unapproved[0].id).toEqual(savedCd1.id) + expect(unapproved[0].name).toEqual(cd1.name) + expect(unapproved[0].approvedBy).toBeNull() + expect(unapproved[0].approvedAt).toBeNull() + expect(unapproved[0].tenantId).toEqual(tenant.id) + }) + + it('Should update credential definition without affecting existing approval status', async (): Promise => { + // 1. Create and approve + const credentialDefinition: NewCredentialDefinition = { + name: 'to_update_approved', + version: 'initial_version', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + icon: asset.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ] + } + let savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + savedCredentialDefinition = await credentialDefinitionRepository.approve(savedCredentialDefinition.id, testUser.id) + const originalApprovalTime = savedCredentialDefinition.approvedAt + + // 2. Update a different field + const newName = 'updated_name_approved' + const updatePayload: NewCredentialDefinition = { + // Spread original *creation* data, not the approved state + ...credentialDefinition, + name: newName, + } + const updatedDefinition = await credentialDefinitionRepository.update(savedCredentialDefinition.id, updatePayload) + + // 3. Assert + expect(updatedDefinition).toBeDefined() + expect(updatedDefinition.name).toEqual(newName) // Name is updated + expect(updatedDefinition.version).toEqual(credentialDefinition.version) // Version is unchanged + expect(updatedDefinition.icon?.id).toEqual(asset.id) // Icon is unchanged + expect(updatedDefinition.tenantId).toEqual(tenant.id) // TenantId is unchanged + + // Verify approval status is PERSISTED + expect(updatedDefinition.approvedBy).toBeDefined() + expect(updatedDefinition.approvedBy?.id).toEqual(testUser.id) + expect(updatedDefinition.approvedAt).toBeDefined() + // Check the timestamp is the *original* approval time, not modified by the update + expect(updatedDefinition.approvedAt?.toISOString()).toEqual(originalApprovalTime?.toISOString()) + }) + + it('Should update credential definition that was not approved', async (): Promise => { + // 1. Create unapproved + const credentialDefinition: NewCredentialDefinition = { + name: 'to_update_unapproved', + version: 'initial_version', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + //revocation: { + // TODO SHOWCASE-80 AnonCredRevocation + //title: 'example_revocation_title', + // description: 'example_revocation_description', + // }, + } + + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + + // 2. Update a different field + const newName = 'updated_name_unapproved' + const updatePayload: NewCredentialDefinition = { + ...credentialDefinition, + name: newName, + } + const updatedDefinition = await credentialDefinitionRepository.update(savedCredentialDefinition.id, updatePayload) + + // 3. Assert + expect(updatedDefinition).toBeDefined() + expect(updatedDefinition.name).toEqual(newName) // Name is updated + expect(updatedDefinition.tenantId).toEqual(tenant.id) // TenantId is unchanged + // Verify approval status is still null/undefined + expect(updatedDefinition.approvedBy).toBeNull() + expect(updatedDefinition.approvedAt).toBeNull() + }) + + // --- Keep existing tests for other functionality (delete, invalid icon, etc.) --- + it('Should save the credential definition without icon to database', async (): Promise => { + const credentialDefinition: NewCredentialDefinition = { + name: 'example_name_no_icon', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + //revocation: { + // TODO SHOWCASE-80 AnonCredRevocation + //title: 'example_revocation_title', + // description: 'example_revocation_description', + // }, + } + + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + expect(savedCredentialDefinition).toBeDefined() + expect(savedCredentialDefinition.name).toEqual(credentialDefinition.name) + expect(savedCredentialDefinition.icon).toBeUndefined() + expect(savedCredentialDefinition.tenantId).toEqual(tenant.id) + expect(savedCredentialDefinition.approvedBy).toBeNull() + expect(savedCredentialDefinition.approvedAt).toBeNull() + }) + + it('Should throw error when saving credential definition with invalid icon id', async (): Promise => { + const unknownIconId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const credentialDefinition: NewCredentialDefinition = { + name: 'invalid_icon_test', + version: '1.0', + icon: unknownIconId, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + await expect(credentialDefinitionRepository.create(credentialDefinition)).rejects.toThrowError( + `No asset found for id: ${unknownIconId}`, + ) + }) + + it('Should delete credential definition from database', async (): Promise => { + const credentialDefinition: NewCredentialDefinition = { + name: 'to_delete', + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + await credentialDefinitionRepository.delete(savedCredentialDefinition.id) + await expect(credentialDefinitionRepository.findById(savedCredentialDefinition.id)).rejects.toThrowError( + `No credential definition found for id: ${savedCredentialDefinition.id}`, + ) + }) + + it('Should throw error when updating credential definition with invalid icon id', async (): Promise => { + const unknownIconId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const credentialDefinition: NewCredentialDefinition = { + name: 'update_invalid_icon', + version: '1.0', + icon: asset.id, // Start with a valid icon + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + } + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + const updatePayload: NewCredentialDefinition = { + ...credentialDefinition, // Use original creation data + icon: unknownIconId, // Set invalid icon for update + } + await expect( + credentialDefinitionRepository.update(savedCredentialDefinition.id, updatePayload), + ).rejects.toThrowError( + `insert or update on table "credentialDefinition" violates foreign key constraint "credentialDefinition_icon_asset_id_fk"`, + ) + }) + + it('Should update credential definition removing the icon', async (): Promise => { + const credentialDefinition: NewCredentialDefinition = { + name: 'remove_icon_update', + version: '1.0', + icon: asset.id, // Start with an icon + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + // revocation: { // TODO SHOWCASE-80 AnonCredRevocation + // title: 'example_revocation_title', + // description: 'example_revocation_description' + // } + } + + const savedCredentialDefinition = await credentialDefinitionRepository.create(credentialDefinition) + expect(savedCredentialDefinition.icon).toBeDefined() + + const updatePayload: NewCredentialDefinition = { + ...credentialDefinition, + icon: null, // Remove icon in update + } + const updatedDefinition = await credentialDefinitionRepository.update(savedCredentialDefinition.id, updatePayload) + + expect(updatedDefinition).toBeDefined() + expect(updatedDefinition.name).toEqual(credentialDefinition.name) + expect(updatedDefinition.tenantId).toEqual(tenant.id) + expect(updatedDefinition.icon).toBeUndefined() // Icon should now be undefined + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/credentialSchema.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/credentialSchema.repository.test.ts new file mode 100644 index 000000000..4adb63ba9 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/credentialSchema.repository.test.ts @@ -0,0 +1,186 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { drizzle } from 'drizzle-orm/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { Container } from 'typedi' +import DatabaseService from '../../../services/DatabaseService' +import CredentialSchemaRepository from '../CredentialSchemaRepository' +import * as schema from '../../schema' +import { CredentialAttributeType, IdentifierType, NewCredentialSchema, Source } from '../../../types' + +describe('Database credential schema repository tests', (): void => { + let client: PGlite + let credentialSchemaRepository: CredentialSchemaRepository + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + + credentialSchemaRepository = Container.get(CredentialSchemaRepository) + }) + + afterEach(async (): Promise => { + Container.reset() + await client.close() + }) + + it('should create and retrieve a credential schema', async (): Promise => { + const newSchema: NewCredentialSchema = { + name: 'example_name', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + source: Source.CREATED, + attributes: [ + { + name: 'example_attribute_name1', + value: 'example_attribute_value1', + type: CredentialAttributeType.STRING, + }, + { + name: 'example_attribute_name2', + value: 'example_attribute_value2', + type: CredentialAttributeType.STRING, + }, + ], + } + + const created = await credentialSchemaRepository.create(newSchema) + expect(created).toBeDefined() + expect(created.id).toBeDefined() + expect(created.name).toBe(newSchema.name) + expect(created.identifier).toBe(newSchema.identifier) + expect(created.identifierType).toBe(newSchema.identifierType) + expect(created.version).toBe(newSchema.version) + expect(created.attributes).toHaveLength(2) + expect(created.source).toBe(newSchema.source) + + const retrieved = await credentialSchemaRepository.findById(created.id) + expect(retrieved).toBeDefined() + expect(retrieved?.id).toBe(created.id) + expect(retrieved?.name).toBe(newSchema.name) + expect(retrieved?.attributes).toHaveLength(2) + expect(retrieved?.attributes[0].name).toBe('example_attribute_name1') + expect(retrieved?.attributes[0].type).toBe(CredentialAttributeType.STRING) + expect(retrieved?.attributes[1].name).toBe('example_attribute_name2') + expect(retrieved?.attributes[1].type).toBe(CredentialAttributeType.STRING) + }) + + it('should update a credential schema', async (): Promise => { + const newSchema: NewCredentialSchema = { + name: 'original_name', + version: 'original_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + source: Source.CREATED, + attributes: [ + { + name: 'original_attribute_name', + value: 'original_attribute_value', + type: CredentialAttributeType.STRING, + }, + ], + } + + const created = await credentialSchemaRepository.create(newSchema) + + const updatedSchema: NewCredentialSchema = { + name: 'updated_name', + version: 'updated_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + source: Source.IMPORTED, + attributes: [ + { + name: 'original_attribute_name', + value: 'original_attribute_value', + type: CredentialAttributeType.STRING, + }, + ], + } + + const updated = await credentialSchemaRepository.update(created.id, updatedSchema) + + expect(updated).toBeDefined() + expect(updated.id).toBe(created.id) + expect(updated.source).toBe(updatedSchema.source) + expect(updated.name).toBe(updatedSchema.name) + expect(updated.version).toBe(updatedSchema.version) + expect(updated.attributes).toHaveLength(updatedSchema.attributes.length) + }) + + it('should delete a credential schema', async (): Promise => { + const newSchema: NewCredentialSchema = { + name: 'schema_to_delete', + version: 'delete_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + source: Source.CREATED, + attributes: [ + { + name: 'delete_test_attribute', + value: 'delete_test_value', + type: CredentialAttributeType.STRING, + }, + ], + } + + const created = await credentialSchemaRepository.create(newSchema) + + await credentialSchemaRepository.delete(created.id) + + try { + await credentialSchemaRepository.findById(created.id) + fail('Should have thrown NotFoundError') + } catch (error) { + expect(error.message).toBe(`No credential schema found for id: ${created.id}`) + } + }) + + it('should retrieve all credential schemas', async (): Promise => { + const schema1: NewCredentialSchema = { + name: 'first_schema', + version: 'first_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + source: Source.CREATED, + attributes: [ + { + name: 'first_attribute', + value: 'first_value', + type: CredentialAttributeType.STRING, + }, + ], + } + + const schema2: NewCredentialSchema = { + name: 'second_schema', + version: 'second_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + source: Source.CREATED, + attributes: [ + { + name: 'second_attribute', + value: 'second_value', + type: CredentialAttributeType.STRING, + }, + ], + } + + await credentialSchemaRepository.create(schema1) + await credentialSchemaRepository.create(schema2) + + const allSchemas = await credentialSchemaRepository.findAll() + + expect(allSchemas.length).toBeGreaterThanOrEqual(2) + expect(allSchemas.some((s) => s.name === 'first_schema')).toBe(true) + expect(allSchemas.some((s) => s.name === 'second_schema')).toBe(true) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/dbTestData.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/dbTestData.ts new file mode 100644 index 000000000..c25bfb9d3 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/dbTestData.ts @@ -0,0 +1,253 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { Container } from 'typedi' + +import CredentialDefinitionService from '../../../services/CredentialDefinitionService' +import DatabaseService from '../../../services/DatabaseService' +import ShowcaseService from '../../../services/ShowcaseService' +import { + Asset, + CredentialAttributeType, + CredentialDefinition, + CredentialSchema, + CredentialType, + IdentifierType, + Issuer, + IssuerType, + NewCredentialDefinition, + NewShowcase, + NewUser, + Persona, + Scenario, + Showcase, + ShowcaseStatus, + StepActionType, + StepType, + Tenant, + TenantType, + User, +} from '../../../types' +import AssetRepository from '../AssetRepository' +import CredentialDefinitionRepository from '../CredentialDefinitionRepository' +import CredentialSchemaRepository from '../CredentialSchemaRepository' +import IssuerRepository from '../IssuerRepository' +import PersonaRepository from '../PersonaRepository' +import ScenarioRepository from '../ScenarioRepository' +import ShowcaseRepository from '../ShowcaseRepository' +import TenantRepository from '../TenantRepository' +import UserRepository from '../UserRepository' + +export async function createMockDatabaseService(database: NodePgDatabase): Promise { + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + return mockDatabaseService as unknown as DatabaseService +} + +export async function createTestAsset(): Promise { + const assetRepository = Container.get(AssetRepository) + return assetRepository.create({ + mediaType: 'image/png', + fileName: 'test.png', + description: 'Test image', + content: Buffer.from('binary data'), + }) +} + +export async function createTestAssetWithContent( + content: Buffer, + mediaType = 'image/png', + fileName = 'test.png', + description = 'Test image', +): Promise { + const assetRepository = Container.get(AssetRepository) + return assetRepository.create({ + mediaType, + fileName, + content, + description, + }) +} + +export async function createTestCredentialSchema(): Promise { + const credentialSchemaRepository = Container.get(CredentialSchemaRepository) + return credentialSchemaRepository.create({ + name: 'example_name', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + attributes: [ + { + name: 'example_attribute_name1', + value: 'example_attribute_value1', + type: CredentialAttributeType.STRING, + }, + { + name: 'example_attribute_name2', + value: 'example_attribute_value2', + type: CredentialAttributeType.STRING, + }, + ], + }) +} + +export async function createTestPersona(asset?: Asset): Promise { + const personaRepository = Container.get(PersonaRepository) + return personaRepository.create({ + name: 'Test Persona', + role: 'Test Role', + description: 'Test description', + headshotImage: asset?.id, + bodyImage: asset?.id, + hidden: false, + }) +} +export async function createTestScenario( + asset: Asset, + persona: Persona, + issuer: Issuer, + credentialDefinitionId: string, + tenantId: string, +): Promise { + const scenarioRepository = Container.get(ScenarioRepository) + return scenarioRepository.create({ + name: 'Test Scenario', + description: 'Test scenario description', + issuer: issuer.id, + steps: [ + { + title: 'Test Step', + description: 'Test step description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'Test Action', + actionType: StepActionType.ACCEPT_CREDENTIAL, + text: 'Test action text', + credentialDefinitionId: credentialDefinitionId, + }, + ], + }, + ], + personas: [persona.id], + hidden: false, + }) +} + +export async function createTestTenant(id = 'test-tenant'): Promise { + const tenantRepository = Container.get(TenantRepository) + return tenantRepository.create({ + id, + oidcIssuer: 'https://auth-server/auth/realms/test', + + tenantType: TenantType.ROOT, + }) +} + +export async function createTestCredentialDefinition( + asset: Asset, + schema: CredentialSchema, + tenantId: string, +): Promise { + const credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + return credentialDefinitionRepository.create({ + name: 'Test Definition', + version: '1.0', + identifierType: IdentifierType.DID, + identifier: 'did:test:123', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: schema.id, + tenantId: tenantId, + }) +} + +export async function createTestIssuer( + asset: Asset, + definition: CredentialDefinition, + schema: CredentialSchema, + tenantId: string, +): Promise { + const issuerRepository = Container.get(IssuerRepository) + return issuerRepository.create({ + name: 'Test Issuer', + type: IssuerType.ARIES, + credentialDefinitions: [definition.id], + credentialSchemas: [schema.id], + description: 'Test issuer description', + organization: 'Test Organization', + logo: asset.id, + tenantId: tenantId, + }) +} + +export async function createTestUser(userName: string): Promise { + const userRepository = Container.get(UserRepository) + const newUser: NewUser = { + userName, + issuer: 'https://auth-server.example.com/auth/realms/BC', + clientId: 'showcase-tenantA', + } + return await userRepository.create(newUser) +} + +export async function createTestShowcase( + tenantId: string, + name: string, + status: ShowcaseStatus = ShowcaseStatus.ACTIVE, + creatorUserId?: string, +): Promise { + // Returning internal Showcase type after creation + // Create necessary dependencies using db helpers first + const asset = await createTestAsset() + const persona = await createTestPersona(asset) + const schema = await createTestCredentialSchema() + // Use renamed db helper + const definition = await createTestCredentialDefinition(asset, schema, tenantId) + const issuer = await createTestIssuer(asset, definition, schema, tenantId) + const scenario = await createTestScenario(asset, persona, issuer, definition.id, tenantId) + + // Use the ShowcaseService to create the showcase + const showcaseService = Container.get(ShowcaseService) + const newShowcase: NewShowcase = { + name: name, + description: `${name} description`, + tenantId: tenantId, + status: status, + hidden: false, + scenarios: [scenario.id], + personas: [persona.id], + credentialDefinitions: [definition.id], + bannerImage: asset.id, + createdBy: creatorUserId, + } + const created = await showcaseService.createShowcase(newShowcase) + + // Re-fetch using repository to ensure relations are populated for return + // (Service create might not return fully populated relations) + const showcaseRepository = Container.get(ShowcaseRepository) + return await showcaseRepository.findById(created.id) +} + +// Helper to create a Credential Definition via the service for API testing +export async function createUnapprovedCredDef(name: string, tenantId: string): Promise { + const asset = await createTestAsset() + const schema = await createTestCredentialSchema() + + // Use the CredentialDefinitionService + const credDefService = Container.get(CredentialDefinitionService) + const newCredDef: NewCredentialDefinition = { + name: name, + version: '1.0', + type: CredentialType.ANONCRED, + credentialSchema: schema.id, + icon: asset.id, + tenantId: tenantId, + } + const created = await credDefService.createCredentialDefinition(newCredDef) + + // Re-fetch using repository to ensure relations are populated + const credentialDefinitionRepo = Container.get(CredentialDefinitionRepository) + return await credentialDefinitionRepo.findById(created.id) +} diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/issuer.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/issuer.repository.test.ts new file mode 100644 index 000000000..0765d18fb --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/issuer.repository.test.ts @@ -0,0 +1,423 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { + Asset, + CredentialAttributeType, + CredentialDefinition, + CredentialSchema, + CredentialType, + IdentifierType, + IssuerType, + NewAsset, + NewCredentialDefinition, + NewCredentialSchema, + NewIssuer, + Tenant, +} from '../../../types' +import * as schema from '../../schema' +import AssetRepository from '../AssetRepository' +import CredentialDefinitionRepository from '../CredentialDefinitionRepository' +import CredentialSchemaRepository from '../CredentialSchemaRepository' +import IssuerRepository from '../IssuerRepository' +import { createTestTenant } from './dbTestData' + +describe('Database issuer repository tests', (): void => { + let client: PGlite + let repository: IssuerRepository + let credentialSchema: CredentialSchema + let credentialDefinition1: CredentialDefinition + let credentialDefinition2: CredentialDefinition + let asset: Asset + let tenant: Tenant + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + repository = Container.get(IssuerRepository) + const assetRepository = Container.get(AssetRepository) + const newAsset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + asset = await assetRepository.create(newAsset) + const credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + const credentialSchemaRepository = Container.get(CredentialSchemaRepository) + + // Create test tenant using dbTestData utility + tenant = await createTestTenant() + + const newCredentialSchema: NewCredentialSchema = { + name: 'example_name', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + attributes: [ + { + name: 'example_attribute_name1', + value: 'example_attribute_value1', + type: CredentialAttributeType.STRING, + }, + { + name: 'example_attribute_name2', + value: 'example_attribute_value2', + type: CredentialAttributeType.STRING, + }, + ], + } + credentialSchema = await credentialSchemaRepository.create(newCredentialSchema) + + const newCredentialDefinition: NewCredentialDefinition = { + tenantId: tenant.id, + name: 'example_name', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + // revocation: { // TODO SHOWCASE-80 AnonCredRevocation + // title: 'example_revocation_title', + // description: 'example_revocation_description' + // } + } + credentialDefinition1 = await credentialDefinitionRepository.create(newCredentialDefinition) + credentialDefinition2 = await credentialDefinitionRepository.create(newCredentialDefinition) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save issuer to database', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + + expect(savedIssuer).toBeDefined() + expect(savedIssuer.name).toEqual(issuer.name) + expect(savedIssuer.type).toEqual(issuer.type) + expect(savedIssuer.description).toEqual(issuer.description) + expect(savedIssuer.organization).toEqual(issuer.organization) + expect(savedIssuer.credentialDefinitions.length).toEqual(2) + expect(savedIssuer.logo).not.toBeNull() + expect(savedIssuer.logo!.id).toBeDefined() + expect(savedIssuer.logo!.mediaType).toEqual(asset.mediaType) + expect(savedIssuer.logo!.fileName).toEqual(asset.fileName) + expect(savedIssuer.logo!.description).toEqual(asset.description) + expect(savedIssuer.logo!.content).toStrictEqual(asset.content) + }) + + it('Should throw error when saving issuer with invalid logo id', async (): Promise => { + const unknownIconId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: unknownIconId, + } + + await expect(repository.create(issuer)).rejects.toThrowError(`No asset found for id: ${unknownIconId}`) + }) + + it('Should throw error when saving issuer with invalid credential definition id', async (): Promise => { + const unknownCredentialDefinitionId = '498e1086-a2ac-4189-b951-fe863d0fe9fc' + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [unknownCredentialDefinitionId], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + await expect(repository.create(issuer)).rejects.toThrowError( + `No credential definition found for id: ${unknownCredentialDefinitionId}`, + ) + }) + + it('Should get issuer by id from database', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const fromDb = await repository.findById(savedIssuer.id) + + expect(fromDb).toBeDefined() + expect(fromDb.name).toEqual(issuer.name) + expect(fromDb.type).toEqual(issuer.type) + expect(fromDb.description).toEqual(issuer.description) + expect(fromDb.organization).toEqual(issuer.organization) + expect(fromDb.credentialDefinitions.length).toEqual(2) + expect(fromDb.logo).not.toBeNull() + expect(fromDb.logo!.id).toBeDefined() + expect(fromDb.logo!.mediaType).toEqual(asset.mediaType) + expect(fromDb.logo!.fileName).toEqual(asset.fileName) + expect(fromDb.logo!.description).toEqual(asset.description) + expect(fromDb.logo!.content).toStrictEqual(asset.content) + }) + + it('Should get all issuers from database', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + } + + const savedIssuer1 = await repository.create(issuer) + expect(savedIssuer1).toBeDefined() + + const savedIssuer2 = await repository.create(issuer) + expect(savedIssuer2).toBeDefined() + + const fromDb = await repository.findAll() + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete issuer from database', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + await repository.delete(savedIssuer.id) + + await expect(repository.findById(savedIssuer.id)).rejects.toThrowError(`No issuer found for id: ${savedIssuer.id}`) + }) + + it('Should update issuer in database', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const newName = 'new_name' + const updatedIssuer = await repository.update(savedIssuer.id, { + ...savedIssuer, + name: newName, + credentialDefinitions: [credentialDefinition1.id], + credentialSchemas: [credentialSchema.id], + logo: savedIssuer.logo?.id, + }) + + expect(updatedIssuer).toBeDefined() + expect(updatedIssuer.name).toEqual(newName) + expect(updatedIssuer.type).toEqual(issuer.type) + expect(updatedIssuer.description).toEqual(issuer.description) + expect(updatedIssuer.organization).toEqual(issuer.organization) + expect(updatedIssuer.credentialDefinitions.length).toEqual(1) + expect(updatedIssuer.logo).not.toBeNull() + expect(updatedIssuer.logo!.id).toBeDefined() + expect(updatedIssuer.logo!.mediaType).toEqual(asset.mediaType) + expect(updatedIssuer.logo!.fileName).toEqual(asset.fileName) + expect(updatedIssuer.logo!.description).toEqual(asset.description) + expect(updatedIssuer.logo!.content).toStrictEqual(asset.content) + }) + + it('Should throw error when updating issuer with invalid logo id', async (): Promise => { + const unknownIconId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const updatedIssuer: NewIssuer = { + ...savedIssuer, + credentialDefinitions: savedIssuer.credentialDefinitions.map((credentialDefinition) => credentialDefinition.id), + credentialSchemas: savedIssuer.credentialSchemas.map((credentialSchema) => credentialSchema.id), + logo: unknownIconId, + } + + await expect(repository.update(savedIssuer.id, updatedIssuer)).rejects.toThrowError( + `No asset found for id: ${unknownIconId}`, + ) + }) + + it('Should throw error when updating issuer with no credential definitions', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const updatedIssuer: NewIssuer = { + ...savedIssuer, + credentialDefinitions: [], + credentialSchemas: savedIssuer.credentialSchemas.map((credentialSchema) => credentialSchema.id), + logo: asset.id, + } + + await expect(repository.update(savedIssuer.id, updatedIssuer)).rejects.toThrowError( + `At least one credential definition is required`, + ) + }) + + it('Should throw error when updating issuer with no credential schemas', async (): Promise => { + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const updatedIssuer: NewIssuer = { + ...savedIssuer, + credentialDefinitions: savedIssuer.credentialDefinitions.map((credentialDefinition) => credentialDefinition.id), + credentialSchemas: [], + logo: asset.id, + } + + await expect(repository.update(savedIssuer.id, updatedIssuer)).rejects.toThrowError( + `At least one credential schema is required`, + ) + }) + + it('Should throw error when updating issuer with invalid credential definition id', async (): Promise => { + const unknownCredentialDefinitionId = '498e1086-a2ac-4189-b951-fe863d0fe9fc' + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const updatedIssuer: NewIssuer = { + ...savedIssuer, + credentialDefinitions: [unknownCredentialDefinitionId], + credentialSchemas: savedIssuer.credentialSchemas.map((credentialSchema) => credentialSchema.id), + logo: asset.id, + } + + await expect(repository.update(savedIssuer.id, updatedIssuer)).rejects.toThrowError( + `No credential definition found for id: ${unknownCredentialDefinitionId}`, + ) + }) + + it('Should throw error when updating issuer with invalid credential schema id', async (): Promise => { + const unknownCredentialSchemaId = '498e1086-a2ac-4189-b951-fe863d0fe9fc' + const issuer: NewIssuer = { + name: 'example_name', + type: IssuerType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedIssuer = await repository.create(issuer) + expect(savedIssuer).toBeDefined() + + const updatedIssuer: NewIssuer = { + ...savedIssuer, + credentialDefinitions: savedIssuer.credentialDefinitions.map((credentialDefinition) => credentialDefinition.id), + credentialSchemas: [unknownCredentialSchemaId], + logo: asset.id, + } + + await expect(repository.update(savedIssuer.id, updatedIssuer)).rejects.toThrowError( + `No credential schema found for id: ${unknownCredentialSchemaId}`, + ) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/persona.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/persona.repository.test.ts new file mode 100644 index 000000000..e3de5ef73 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/persona.repository.test.ts @@ -0,0 +1,284 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { Asset, NewAsset, NewPersona } from '../../../types' +import * as schema from '../../schema' +import AssetRepository from '../AssetRepository' +import PersonaRepository from '../PersonaRepository' + +describe('Database persona repository tests', (): void => { + let client: PGlite + let personaRepository: PersonaRepository + let asset: Asset + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + personaRepository = Container.get(PersonaRepository) + const assetRepository = Container.get(AssetRepository) + const newAsset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + asset = await assetRepository.create(newAsset) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save persona to database', async (): Promise => { + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: true, + } + + const savedPersona = await personaRepository.create(persona) + + expect(savedPersona).toBeDefined() + expect(savedPersona.name).toEqual(persona.name) + expect(savedPersona.slug).toEqual('john-doe') + expect(savedPersona.role).toEqual(persona.role) + expect(savedPersona.description).toEqual(persona.description) + expect(savedPersona.hidden).toEqual(persona.hidden) + expect(savedPersona.headshotImage).toBeDefined() + expect(savedPersona.headshotImage!.id).toBeDefined() + expect(savedPersona.headshotImage!.mediaType).toEqual(asset.mediaType) + expect(savedPersona.headshotImage!.fileName).toEqual(asset.fileName) + expect(savedPersona.headshotImage!.description).toEqual(asset.description) + expect(savedPersona.headshotImage!.content).toStrictEqual(asset.content) + expect(savedPersona.bodyImage).toBeDefined() + expect(savedPersona.bodyImage!.id).toBeDefined() + expect(savedPersona.bodyImage!.mediaType).toEqual(asset.mediaType) + expect(savedPersona.bodyImage!.fileName).toEqual(asset.fileName) + expect(savedPersona.bodyImage!.description).toEqual(asset.description) + expect(savedPersona.bodyImage!.content).toStrictEqual(asset.content) + }) + + it('Should save persona without images to database', async (): Promise => { + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + hidden: false, + } + + const savedPersona = await personaRepository.create(persona) + + expect(savedPersona).toBeDefined() + expect(savedPersona.name).toEqual(persona.name) + expect(savedPersona.role).toEqual(persona.role) + expect(savedPersona.description).toEqual(persona.description) + expect(savedPersona.headshotImage).toBeNull() + expect(savedPersona.bodyImage).toBeNull() + }) + + it('Should throw error when saving persona with invalid headshot image id', async (): Promise => { + const unknownImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: unknownImageId, + hidden: false, + } + + await expect(personaRepository.create(persona)).rejects.toThrowError(`No asset found for id: ${unknownImageId}`) + }) + + it('Should throw error when saving persona with invalid body image id', async (): Promise => { + const unknownImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + bodyImage: unknownImageId, + hidden: false, + } + + await expect(personaRepository.create(persona)).rejects.toThrowError(`No asset found for id: ${unknownImageId}`) + }) + + it('Should get persona by id from database', async (): Promise => { + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } + + const savedPersona = await personaRepository.create(persona) + expect(savedPersona).toBeDefined() + + const fromDb = await personaRepository.findById(savedPersona.id) + + expect(fromDb).toBeDefined() + expect(fromDb.name).toEqual(persona.name) + expect(fromDb.role).toEqual(persona.role) + expect(fromDb.description).toEqual(persona.description) + expect(fromDb.headshotImage).toBeDefined() + expect(fromDb.headshotImage!.id).toBeDefined() + expect(fromDb.headshotImage!.mediaType).toEqual(asset.mediaType) + expect(fromDb.headshotImage!.fileName).toEqual(asset.fileName) + expect(fromDb.headshotImage!.description).toEqual(asset.description) + expect(fromDb.headshotImage!.content).toStrictEqual(asset.content) + expect(fromDb.bodyImage).toBeDefined() + expect(fromDb.bodyImage!.id).toBeDefined() + expect(fromDb.bodyImage!.mediaType).toEqual(asset.mediaType) + expect(fromDb.bodyImage!.fileName).toEqual(asset.fileName) + expect(fromDb.bodyImage!.description).toEqual(asset.description) + expect(fromDb.bodyImage!.content).toStrictEqual(asset.content) + }) + + it('Should get all personas from database', async (): Promise => { + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } + + const savedPersona1 = await personaRepository.create(persona) + expect(savedPersona1).toBeDefined() + + const savedPersona2 = await personaRepository.create(persona) + expect(savedPersona2).toBeDefined() + + const fromDb = await personaRepository.findAll() + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete persona from database', async (): Promise => { + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } + + const savedPersona = await personaRepository.create(persona) + expect(savedPersona).toBeDefined() + + await personaRepository.delete(savedPersona.id) + + await expect(personaRepository.findById(savedPersona.id)).rejects.toThrowError( + `No persona found for id: ${savedPersona.id}`, + ) + }) + + it('Should update persona in database', async (): Promise => { + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: true, + } + + const savedPersona = await personaRepository.create(persona) + expect(savedPersona).toBeDefined() + + const newName = 'Jane Doe' + const updatedPersona = await personaRepository.update(savedPersona.id, { + ...savedPersona, + headshotImage: savedPersona.headshotImage!.id, + bodyImage: savedPersona.bodyImage!.id, + name: newName, + }) + + expect(updatedPersona).toBeDefined() + expect(updatedPersona.name).toEqual(newName) + expect(updatedPersona.slug).toEqual('jane-doe') + expect(updatedPersona.role).toEqual(persona.role) + expect(updatedPersona.description).toEqual(persona.description) + expect(updatedPersona.hidden).toEqual(persona.hidden) + expect(updatedPersona.headshotImage).toBeDefined() + expect(updatedPersona.headshotImage!.id).toBeDefined() + expect(updatedPersona.headshotImage!.mediaType).toEqual(asset.mediaType) + expect(updatedPersona.headshotImage!.fileName).toEqual(asset.fileName) + expect(updatedPersona.headshotImage!.description).toEqual(asset.description) + expect(updatedPersona.headshotImage!.content).toStrictEqual(asset.content) + expect(updatedPersona.bodyImage).toBeDefined() + expect(updatedPersona.bodyImage!.id).toBeDefined() + expect(updatedPersona.bodyImage!.mediaType).toEqual(asset.mediaType) + expect(updatedPersona.bodyImage!.fileName).toEqual(asset.fileName) + expect(updatedPersona.bodyImage!.description).toEqual(asset.description) + expect(updatedPersona.bodyImage!.content).toStrictEqual(asset.content) + }) + + it('Should throw error when updating persona with invalid headshot image id', async (): Promise => { + const unknownImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } + + const savedPersona = await personaRepository.create(persona) + expect(savedPersona).toBeDefined() + + const updatedPersona: NewPersona = { + ...savedPersona, + headshotImage: unknownImageId, + bodyImage: savedPersona.bodyImage!.id, + } + + await expect(personaRepository.update(savedPersona.id, updatedPersona)).rejects.toThrowError( + `No asset found for id: ${unknownImageId}`, + ) + }) + + it('Should throw error when updating persona with invalid body image id', async (): Promise => { + const unknownImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const persona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } + + const savedPersona = await personaRepository.create(persona) + expect(savedPersona).toBeDefined() + + const updatedPersona: NewPersona = { + ...savedPersona, + headshotImage: savedPersona.headshotImage!.id, + bodyImage: unknownImageId, + } + + await expect(personaRepository.update(savedPersona.id, updatedPersona)).rejects.toThrowError( + `No asset found for id: ${unknownImageId}`, + ) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/relyingParty.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/relyingParty.repository.test.ts new file mode 100644 index 000000000..696260a63 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/relyingParty.repository.test.ts @@ -0,0 +1,317 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { + Asset, + CredentialDefinition, + CredentialType, + IdentifierType, + NewCredentialDefinition, + RelyingPartyType, + Tenant, + NewRelyingParty, +} from '../../../types' +import * as schema from '../../schema' +import CredentialDefinitionRepository from '../CredentialDefinitionRepository' +import RelyingPartyRepository from '../RelyingPartyRepository' +import { createTestAsset, createTestCredentialSchema, createTestTenant } from './dbTestData' + +describe('Database relying party repository tests', (): void => { + let client: PGlite + let repository: RelyingPartyRepository + let credentialDefinition: CredentialDefinition + let asset: Asset + let tenant: Tenant + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + repository = Container.get(RelyingPartyRepository) + + // Create prerequisites using test utilities + asset = await createTestAsset() + const credentialSchema = await createTestCredentialSchema() + tenant = await createTestTenant() + + const credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + const newCredentialDefinition: NewCredentialDefinition = { + name: 'Test Definition', + version: '1.0', + identifierType: IdentifierType.DID, + identifier: 'did:test:123', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + tenantId: tenant.id, // Using the tenant ID + + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + // revocation: { // TODO SHOWCASE-80 AnonCredRevocation + // title: 'example_revocation_title', + // description: 'example_revocation_description' + // } + } + credentialDefinition = await credentialDefinitionRepository.create(newCredentialDefinition) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save relying party to database', async (): Promise => { + const relyingPartyData = { + name: 'Test Relying Party', + tenantId: tenant.id, + description: 'Test relying party description', + type: RelyingPartyType.ARIES, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + } + + const savedRelyingParty = await repository.create(relyingPartyData) + + expect(savedRelyingParty).toBeDefined() + expect(savedRelyingParty.name).toEqual(relyingPartyData.name) + expect(savedRelyingParty.type).toEqual(relyingPartyData.type) + expect(savedRelyingParty.organization).toEqual(relyingPartyData.organization) + expect(savedRelyingParty.logo).toBeDefined() + expect(savedRelyingParty.logo!.id).toBeDefined() + expect(savedRelyingParty.credentialDefinitions.length).toBe(1) + expect(savedRelyingParty.credentialDefinitions[0].id).toEqual(credentialDefinition.id) + }) + + it('Should throw error when saving relying party with invalid logo id', async (): Promise => { + const unknownIconId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const relyingParty: NewRelyingParty = { + name: 'example_name', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition.id], + description: 'example_description', + organization: 'example_organization', + logo: unknownIconId, + } + + await expect(repository.create(relyingParty)).rejects.toThrowError(`No asset found for id: ${unknownIconId}`) + }) + + it('Should throw error when saving relying party with invalid credential definition id', async (): Promise => { + const unknownCredentialDefinitionId = '498e1086-a2ac-4189-b951-fe863d0fe9fc' + const relyingParty: NewRelyingParty = { + name: 'example_name', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [unknownCredentialDefinitionId], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + await expect(repository.create(relyingParty)).rejects.toThrowError( + `No credential definition found for id: ${unknownCredentialDefinitionId}`, + ) + }) + + it('Should get relying party by id from database', async (): Promise => { + const relyingPartyData = { + name: 'Test Relying Party', + description: 'Test relying party description', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + } + + const savedRelyingParty = await repository.create(relyingPartyData) + expect(savedRelyingParty).toBeDefined() + + const fromDb = await repository.findById(savedRelyingParty.id) + + expect(fromDb).toBeDefined() + expect(fromDb.name).toEqual(relyingPartyData.name) + expect(fromDb.description).toEqual(relyingPartyData.description) + expect(fromDb.credentialDefinitions.length).toBe(1) + expect(fromDb.credentialDefinitions[0].id).toEqual(credentialDefinition.id) + }) + + it('Should get all relying parties from database', async (): Promise => { + const relyingPartyData = { + name: 'Test Relying Party', + description: 'Test relying party description', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + } + + const savedRelyingParty1 = await repository.create(relyingPartyData) + expect(savedRelyingParty1).toBeDefined() + + const savedRelyingParty2 = await repository.create(relyingPartyData) + expect(savedRelyingParty2).toBeDefined() + + const fromDb = await repository.findAll() + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete relying party from database', async (): Promise => { + const relyingPartyData = { + name: 'Test Relying Party', + description: 'Test relying party description', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + } + + const savedRelyingParty = await repository.create(relyingPartyData) + expect(savedRelyingParty).toBeDefined() + + await repository.delete(savedRelyingParty.id) + + await expect(repository.findById(savedRelyingParty.id)).rejects.toThrowError( + `No relying party found for id: ${savedRelyingParty.id}`, + ) + }) + + it('Should update relying party in database', async (): Promise => { + const relyingPartyData = { + name: 'Test Relying Party', + description: 'Test relying party description', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + organization: 'Test Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + } + + const savedRelyingParty = await repository.create(relyingPartyData) + expect(savedRelyingParty).toBeDefined() + + const updatedRelyingParty = await repository.update(savedRelyingParty.id, { + name: 'Updated Name', + description: 'Updated description', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + organization: 'Updated Organization', + logo: asset.id, + credentialDefinitions: [credentialDefinition.id], + }) + + expect(updatedRelyingParty).toBeDefined() + expect(updatedRelyingParty.name).toEqual('Updated Name') + expect(updatedRelyingParty.description).toEqual('Updated description') + expect(updatedRelyingParty.organization).toEqual('Updated Organization') + expect(updatedRelyingParty.credentialDefinitions.length).toBe(1) + }) + + it('Should throw error when updating relying party with invalid logo id', async (): Promise => { + const unknownIconId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const relyingParty: NewRelyingParty = { + name: 'example_name', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedRelyingParty = await repository.create(relyingParty) + expect(savedRelyingParty).toBeDefined() + + const updatedRelyingParty: NewRelyingParty = { + ...savedRelyingParty, + credentialDefinitions: savedRelyingParty.credentialDefinitions.map( + (credentialDefinition) => credentialDefinition.id, + ), + logo: unknownIconId, + } + + await expect(repository.update(savedRelyingParty.id, updatedRelyingParty)).rejects.toThrowError( + `No asset found for id: ${unknownIconId}`, + ) + }) + + it('Should throw error when updating relying party with no credential definitions', async (): Promise => { + const relyingParty: NewRelyingParty = { + name: 'example_name', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedRelyingParty = await repository.create(relyingParty) + expect(savedRelyingParty).toBeDefined() + + const updatedRelyingParty: NewRelyingParty = { + ...savedRelyingParty, + credentialDefinitions: [], + logo: asset.id, + } + + await expect(repository.update(savedRelyingParty.id, updatedRelyingParty)).rejects.toThrowError( + `At least one credential definition is required`, + ) + }) + + it('Should throw error when updating relying party with invalid credential definition id', async (): Promise => { + const unknownCredentialDefinitionId = '498e1086-a2ac-4189-b951-fe863d0fe9fc' + const relyingParty: NewRelyingParty = { + name: 'example_name', + type: RelyingPartyType.ARIES, + tenantId: tenant.id, + credentialDefinitions: [credentialDefinition.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + + const savedRelyingParty = await repository.create(relyingParty) + expect(savedRelyingParty).toBeDefined() + + const updatedRelyingParty: NewRelyingParty = { + ...savedRelyingParty, + credentialDefinitions: [unknownCredentialDefinitionId], + logo: asset.id, + } + + await expect(repository.update(savedRelyingParty.id, updatedRelyingParty)).rejects.toThrowError( + `No credential definition found for id: ${unknownCredentialDefinitionId}`, + ) + }) + + it('Should throw error when accessing non-existent relying party', async (): Promise => { + const nonExistentId = '00000000-0000-0000-0000-000000000000' + + await expect(repository.findById(nonExistentId)).rejects.toThrowError( + `No relying party found for id: ${nonExistentId}`, + ) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/scenario.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/scenario.repository.test.ts new file mode 100644 index 000000000..e9d0f3618 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/scenario.repository.test.ts @@ -0,0 +1,3815 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import AssetRepository from '../../../database/repositories/AssetRepository' +import CredentialDefinitionRepository from '../../../database/repositories/CredentialDefinitionRepository' +import IssuerRepository from '../../../database/repositories/IssuerRepository' +import ScenarioRepository from '../../../database/repositories/ScenarioRepository' +import * as schema from '../../../database/schema' +import DatabaseService from '../../../services/DatabaseService' +import { + AriesOOBAction, + Asset, + ButtonAction, + CredentialAttributeType, + CredentialDefinition, + CredentialType, + IdentifierType, + IssuanceScenario, + Issuer, + IssuerType, + NewAriesOOBAction, + NewAsset, + NewButtonAction, + NewChooseWalletAction, + NewCredentialDefinition, + NewCredentialSchema, + NewIssuanceScenario, + NewIssuer, + NewPersona, + NewPresentationScenario, + NewRelyingParty, + NewSetupConnectionAction, + NewStep, + Persona, + PresentationScenario, + RelyingParty, + RelyingPartyType, + ScenarioType, + StepActionType, + StepType, +} from '../../../types' +import CredentialSchemaRepository from '../CredentialSchemaRepository' +import PersonaRepository from '../PersonaRepository' +import RelyingPartyRepository from '../RelyingPartyRepository' +import { createTestTenant } from './dbTestData' + +describe('Database scenario repository tests', (): void => { + let client: PGlite + let repository: ScenarioRepository + let issuer: Issuer + let relyingParty: RelyingParty + let asset: Asset + let persona1: Persona + let persona2: Persona + let credentialDefinition: CredentialDefinition + let tenantId: string + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + repository = Container.get(ScenarioRepository) + + // Create a tenant for testing + const tenant = await createTestTenant('test-tenant') + tenantId = tenant.id + + const issuerRepository = Container.get(IssuerRepository) + const relyingPartyRepository = Container.get(RelyingPartyRepository) + const credentialDefinitionRepository = Container.get(CredentialDefinitionRepository) + const credentialSchemaRepository = Container.get(CredentialSchemaRepository) + const assetRepository = Container.get(AssetRepository) + const newAsset: NewAsset = { + mediaType: 'image/png', + fileName: 'image.png', + description: 'some image', + content: Buffer.from('some binary data'), + } + asset = await assetRepository.create(newAsset) + + const newCredentialSchema: NewCredentialSchema = { + name: 'example_name', + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + attributes: [ + { + name: 'example_attribute_name1', + value: 'example_attribute_value1', + type: CredentialAttributeType.STRING, + }, + { + name: 'example_attribute_name2', + value: 'example_attribute_value2', + type: CredentialAttributeType.STRING, + }, + ], + } + const credentialSchema = await credentialSchemaRepository.create(newCredentialSchema) + + const newCredentialDefinition: NewCredentialDefinition = { + name: 'example_name', + tenantId: tenantId, + version: 'example_version', + identifierType: IdentifierType.DID, + identifier: 'did:sov:XUeUZauFLeBNofY3NhaZCB', + icon: asset.id, + type: CredentialType.ANONCRED, + credentialSchema: credentialSchema.id, + // representations: [ + // { // TODO SHOWCASE-81 OCARepresentation + // + // }, + // { // TODO SHOWCASE-81 OCARepresentation + // + // } + // ], + revocation: { + // TODO SHOWCASE-80 AnonCredRevocation + title: 'example_revocation_title', + description: 'example_revocation_description', + }, + } + credentialDefinition = await credentialDefinitionRepository.create(newCredentialDefinition) + const newIssuer: NewIssuer = { + tenantId, + name: 'example_name', + type: IssuerType.ARIES, + credentialDefinitions: [credentialDefinition.id], + credentialSchemas: [credentialSchema.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id + } + issuer = await issuerRepository.create(newIssuer) + const newRelyingParty: NewRelyingParty = { + tenantId, + name: 'example_name', + type: RelyingPartyType.ARIES, + credentialDefinitions: [credentialDefinition.id], + description: 'example_description', + organization: 'example_organization', + logo: asset.id, + } + relyingParty = await relyingPartyRepository.create(newRelyingParty) + const personaRepository = Container.get(PersonaRepository) + const newPersona: NewPersona = { + name: 'John Doe', + role: 'Software Engineer', + description: 'Experienced developer', + headshotImage: asset.id, + bodyImage: asset.id, + hidden: false, + } + persona1 = await personaRepository.create(newPersona) + persona2 = await personaRepository.create(newPersona) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save issuance scenario to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id, persona2.id], + bannerImage: asset.id, + hidden: true, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.name).toEqual(issuanceScenario.name) + expect(savedIssuanceScenario.slug).toEqual('example-name') + expect(savedIssuanceScenario.description).toEqual(issuanceScenario.description) + expect(savedIssuanceScenario.hidden).toEqual(issuanceScenario.hidden) + expect(savedIssuanceScenario.steps).toBeDefined() + expect(savedIssuanceScenario.steps.length).toEqual(2) + expect(savedIssuanceScenario.steps[0].title).toEqual(issuanceScenario.steps[0].title) + expect(savedIssuanceScenario.steps[0].order).toEqual(issuanceScenario.steps[0].order) + expect(savedIssuanceScenario.steps[0].type).toEqual(issuanceScenario.steps[0].type) + expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions![0].id).toBeDefined() + expect(savedIssuanceScenario.steps[0].actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(savedIssuanceScenario.steps[0].actions![0].actionType).toEqual( + issuanceScenario.steps[0].actions![0].actionType, + ) + expect(savedIssuanceScenario.steps[0].actions![0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + if (savedIssuanceScenario.steps[0].actions![0].actionType === StepActionType.ARIES_OOB) { + const action = savedIssuanceScenario.steps[0].actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + expect(savedIssuanceScenario.steps[0].asset).not.toBeNull() + expect(savedIssuanceScenario.steps[0].asset!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.steps[0].asset!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.steps[0].asset!.description).toEqual(asset.description) + expect(savedIssuanceScenario.steps[0].asset!.content).toStrictEqual(asset.content) + expect((savedIssuanceScenario).issuer).not.toBeNull() + expect((savedIssuanceScenario).issuer!.name).toEqual(issuer.name) + expect((savedIssuanceScenario).issuer!.description).toEqual(issuer.description) + expect((savedIssuanceScenario).issuer!.organization).toEqual(issuer.organization) + expect((savedIssuanceScenario).issuer!.logo).not.toBeNull() + expect(savedIssuanceScenario.personas).toBeDefined() + expect(savedIssuanceScenario.personas.length).toEqual(2) + expect(savedIssuanceScenario.personas[0].name).toEqual(persona1.name) + expect(savedIssuanceScenario.personas[0].role).toEqual(persona1.role) + expect(savedIssuanceScenario.personas[0].description).toEqual(persona1.description) + expect(savedIssuanceScenario.personas[0].headshotImage).not.toBeNull() + expect(savedIssuanceScenario.personas[0].headshotImage!.id).toBeDefined() + expect(savedIssuanceScenario.personas[0].headshotImage!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.personas[0].headshotImage!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.personas[0].headshotImage!.description).toEqual(asset.description) + expect(savedIssuanceScenario.personas[0].headshotImage!.content).toStrictEqual(asset.content) + expect(savedIssuanceScenario.personas[0].bodyImage).not.toBeNull() + expect(savedIssuanceScenario.personas[0].bodyImage!.id).toBeDefined() + expect(savedIssuanceScenario.personas[0].bodyImage!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.personas[0].bodyImage!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.personas[0].bodyImage!.description).toEqual(asset.description) + expect(savedIssuanceScenario.personas[0].bodyImage!.content).toStrictEqual(asset.content) + expect(savedIssuanceScenario.bannerImage!.id).toBeDefined() + expect(savedIssuanceScenario.bannerImage!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.bannerImage!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.bannerImage!.description).toEqual(asset.description) + expect(savedIssuanceScenario.bannerImage!.content).toStrictEqual(asset.content) + }) + + it('Should save issuance scenario to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id, persona2.id], + bannerImage: asset.id, + hidden: true, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.name).toEqual(issuanceScenario.name) + expect(savedIssuanceScenario.slug).toEqual('example-name') + expect(savedIssuanceScenario.description).toEqual(issuanceScenario.description) + expect(savedIssuanceScenario.hidden).toEqual(issuanceScenario.hidden) + expect(savedIssuanceScenario.steps).toBeDefined() + expect(savedIssuanceScenario.steps.length).toEqual(2) + expect(savedIssuanceScenario.steps[0].title).toEqual(issuanceScenario.steps[0].title) + expect(savedIssuanceScenario.steps[0].order).toEqual(issuanceScenario.steps[0].order) + expect(savedIssuanceScenario.steps[0].type).toEqual(issuanceScenario.steps[0].type) + expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions![0].id).toBeDefined() + expect(savedIssuanceScenario.steps[0].actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(savedIssuanceScenario.steps[0].actions![0].actionType).toEqual( + issuanceScenario.steps[0].actions![0].actionType, + ) + expect(savedIssuanceScenario.steps[0].actions![0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + + if (savedIssuanceScenario.steps[0].actions![0].actionType === StepActionType.ARIES_OOB) { + const action = savedIssuanceScenario.steps[0].actions![0] as AriesOOBAction + expect(action.proofRequest).not.toBeNull() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.name).toEqual(action.proofRequest.predicates!.predicate1.name) + expect(action.proofRequest.predicates!.predicate1.type).toEqual(action.proofRequest.predicates!.predicate1.type) + expect(action.proofRequest.predicates!.predicate1.value).toEqual( + action.proofRequest.predicates!.predicate1.value, + ) + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + + expect(savedIssuanceScenario.steps[0].asset).not.toBeNull() + expect(savedIssuanceScenario.steps[0].asset!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.steps[0].asset!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.steps[0].asset!.description).toEqual(asset.description) + expect(savedIssuanceScenario.steps[0].asset!.content).toStrictEqual(asset.content) + expect((savedIssuanceScenario).issuer).not.toBeNull() + expect((savedIssuanceScenario).issuer!.name).toEqual(issuer.name) + expect((savedIssuanceScenario).issuer!.description).toEqual(issuer.description) + expect((savedIssuanceScenario).issuer!.organization).toEqual(issuer.organization) + expect((savedIssuanceScenario).issuer!.logo).not.toBeNull() + expect(savedIssuanceScenario.personas).toBeDefined() + expect(savedIssuanceScenario.personas.length).toEqual(2) + expect(savedIssuanceScenario.personas[0].name).toEqual(persona1.name) + expect(savedIssuanceScenario.personas[0].role).toEqual(persona1.role) + expect(savedIssuanceScenario.personas[0].description).toEqual(persona1.description) + expect(savedIssuanceScenario.personas[0].headshotImage).not.toBeNull() + expect(savedIssuanceScenario.personas[0].headshotImage!.id).toBeDefined() + expect(savedIssuanceScenario.personas[0].headshotImage!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.personas[0].headshotImage!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.personas[0].headshotImage!.description).toEqual(asset.description) + expect(savedIssuanceScenario.personas[0].headshotImage!.content).toStrictEqual(asset.content) + expect(savedIssuanceScenario.personas[0].bodyImage).not.toBeNull() + expect(savedIssuanceScenario.personas[0].bodyImage!.id).toBeDefined() + expect(savedIssuanceScenario.personas[0].bodyImage!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.personas[0].bodyImage!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.personas[0].bodyImage!.description).toEqual(asset.description) + expect(savedIssuanceScenario.personas[0].bodyImage!.content).toStrictEqual(asset.content) + expect(savedIssuanceScenario.bannerImage!.id).toBeDefined() + expect(savedIssuanceScenario.bannerImage!.mediaType).toEqual(asset.mediaType) + expect(savedIssuanceScenario.bannerImage!.fileName).toEqual(asset.fileName) + expect(savedIssuanceScenario.bannerImage!.description).toEqual(asset.description) + expect(savedIssuanceScenario.bannerImage!.content).toStrictEqual(asset.content) + }) + + it('Should throw error when saving scenario with no steps', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [], + personas: [persona1.id], + hidden: false, + } + + await expect(repository.create(issuanceScenario)).rejects.toThrowError(`At least one step is required`) + }) + + it('Should throw error when saving scenario with invalid issuer id', async (): Promise => { + const unknownIssuerId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: unknownIssuerId, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + await expect(repository.create(issuanceScenario)).rejects.toThrowError(`No issuer found for id: ${unknownIssuerId}`) + }) + + it('Should throw error when saving scenario with banner image id', async (): Promise => { + const unknownBannerImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + bannerImage: unknownBannerImageId, + hidden: false, + } + + await expect(repository.create(issuanceScenario)).rejects.toThrowError( + `No asset found for id: ${unknownBannerImageId}`, + ) + }) + + it('Should throw error when saving scenario with duplicate step order', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + await expect(repository.create(issuanceScenario)).rejects.toThrowError( + 'duplicate key value violates unique constraint "step_order_scenario_unique"', + ) // FIXME would be nice if we can set a custom error message returns by a constraint + }) + + it('Should throw error when saving scenario with invalid persona id', async (): Promise => { + const unknownPersonaId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [unknownPersonaId], + hidden: false, + } + + await expect(repository.create(issuanceScenario)).rejects.toThrowError( + `No persona found for id: ${unknownPersonaId}`, + ) + }) + + it('Should throw error when saving scenario with no personas', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [], + hidden: false, + } + + await expect(repository.create(issuanceScenario)).rejects.toThrowError(`At least one persona is required`) + }) + + it('Should get scenario by id from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id, persona2.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const fromDb = await repository.findById(savedIssuanceScenario.id) + + expect(fromDb).toBeDefined() + expect(fromDb.name).toEqual(issuanceScenario.name) + expect(fromDb.description).toEqual(issuanceScenario.description) + expect(fromDb.hidden).toEqual(issuanceScenario.hidden) + expect(fromDb.steps).toBeDefined() + expect(fromDb.steps.length).toEqual(2) + + if (fromDb.steps[0].actions![0].actionType === StepActionType.ARIES_OOB) { + const action = fromDb.steps[0].actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + + expect(fromDb.personas).toBeDefined() + expect(fromDb.personas.length).toEqual(2) + expect(fromDb.personas[0].name).toEqual(persona1.name) + expect(fromDb.personas[0].role).toEqual(persona1.role) + expect(fromDb.personas[0].description).toEqual(persona1.description) + expect(fromDb.personas[0].headshotImage).not.toBeNull() + expect(fromDb.personas[0].headshotImage!.id).toBeDefined() + expect(fromDb.personas[0].headshotImage!.mediaType).toEqual(asset.mediaType) + expect(fromDb.personas[0].headshotImage!.fileName).toEqual(asset.fileName) + expect(fromDb.personas[0].headshotImage!.description).toEqual(asset.description) + expect(fromDb.personas[0].headshotImage!.content).toStrictEqual(asset.content) + expect(fromDb.personas[0].bodyImage).not.toBeNull() + expect(fromDb.personas[0].bodyImage!.id).toBeDefined() + expect(fromDb.personas[0].bodyImage!.mediaType).toEqual(asset.mediaType) + expect(fromDb.personas[0].bodyImage!.fileName).toEqual(asset.fileName) + expect(fromDb.personas[0].bodyImage!.description).toEqual(asset.description) + expect(fromDb.personas[0].bodyImage!.content).toStrictEqual(asset.content) + }) + + it('Should get all scenarios from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id, persona2.id], + hidden: false, + } + + const savedIssuanceScenario1 = await repository.create(issuanceScenario) + expect(savedIssuanceScenario1).toBeDefined() + + const savedIssuanceScenario2 = await repository.create(issuanceScenario) + expect(savedIssuanceScenario2).toBeDefined() + + const fromDb = await repository.findAll({ filter: { scenarioType: ScenarioType.ISSUANCE } }) + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete scenario from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + await repository.delete(savedIssuanceScenario.id) + + await expect(repository.findById(savedIssuanceScenario.id)).rejects.toThrowError( + `No scenario found for id: ${savedIssuanceScenario.id}`, + ) + }) + + it('Should update scenario in database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id, persona2.id], + hidden: true, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedIssuanceScenario: NewIssuanceScenario = { + ...savedIssuanceScenario, + name: 'new_name', + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + // credentialDefinitionIdentifierType: credentialDefinition.identifierType, + // credentialDefinitionIdentifier: credentialDefinition.identifier, + actions: [ + { + title: 'example_title1', + actionType: StepActionType.ARIES_OOB, + text: 'example_text1', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + { + title: 'example_title2', + actionType: StepActionType.ARIES_OOB, + text: 'example_text2', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + issuer: (savedIssuanceScenario).issuer!.id, + personas: [persona1.id], + bannerImage: null, + } + const updatedIssuanceScenarioResult = await repository.update(savedIssuanceScenario.id, updatedIssuanceScenario) + + expect(updatedIssuanceScenarioResult).toBeDefined() + expect(updatedIssuanceScenarioResult.name).toEqual(updatedIssuanceScenario.name) + expect(updatedIssuanceScenarioResult.slug).toEqual('new-name') + expect(updatedIssuanceScenarioResult.description).toEqual(updatedIssuanceScenario.description) + expect(updatedIssuanceScenarioResult.hidden).toEqual(updatedIssuanceScenario.hidden) + expect(updatedIssuanceScenarioResult.steps).toBeDefined() + expect(updatedIssuanceScenarioResult.steps.length).toEqual(1) + expect(updatedIssuanceScenarioResult.steps[0].title).toEqual(updatedIssuanceScenario.steps[0].title) + expect(updatedIssuanceScenarioResult.steps[0].order).toEqual(updatedIssuanceScenario.steps[0].order) + expect(updatedIssuanceScenarioResult.steps[0].type).toEqual(updatedIssuanceScenario.steps[0].type) + expect(updatedIssuanceScenarioResult.steps[0].actions!.length).toEqual(2) + expect(updatedIssuanceScenarioResult.steps[0].actions![0].id).toBeDefined() + expect(updatedIssuanceScenarioResult.steps[0].actions![0].title).toEqual( + updatedIssuanceScenario.steps[0].actions![0].title, + ) + expect(updatedIssuanceScenarioResult.steps[0].actions![0].actionType).toEqual( + updatedIssuanceScenario.steps[0].actions![0].actionType, + ) + expect(updatedIssuanceScenarioResult.steps[0].actions![0].text).toEqual( + updatedIssuanceScenario.steps[0].actions![0].text, + ) + expect(updatedIssuanceScenarioResult.steps[0].asset).not.toBeNull() + expect(updatedIssuanceScenarioResult.steps[0].asset!.mediaType).toEqual(asset.mediaType) + expect(updatedIssuanceScenarioResult.steps[0].asset!.fileName).toEqual(asset.fileName) + expect(updatedIssuanceScenarioResult.steps[0].asset!.description).toEqual(asset.description) + expect(updatedIssuanceScenarioResult.steps[0].asset!.content).toStrictEqual(asset.content) + if (updatedIssuanceScenarioResult.steps[0].actions![0].actionType === StepActionType.ARIES_OOB) { + const action = updatedIssuanceScenarioResult.steps[0].actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + expect(updatedIssuanceScenarioResult.personas).toBeDefined() + expect(updatedIssuanceScenarioResult.personas.length).toEqual(1) + expect(updatedIssuanceScenarioResult.personas[0].name).toEqual(persona1.name) + expect(updatedIssuanceScenarioResult.personas[0].role).toEqual(persona1.role) + expect(updatedIssuanceScenarioResult.personas[0].description).toEqual(persona1.description) + expect(updatedIssuanceScenarioResult.personas[0].headshotImage).not.toBeNull() + expect(updatedIssuanceScenarioResult.personas[0].headshotImage!.id).toBeDefined() + expect(updatedIssuanceScenarioResult.personas[0].headshotImage!.mediaType).toEqual(asset.mediaType) + expect(updatedIssuanceScenarioResult.personas[0].headshotImage!.fileName).toEqual(asset.fileName) + expect(updatedIssuanceScenarioResult.personas[0].headshotImage!.description).toEqual(asset.description) + expect(updatedIssuanceScenarioResult.personas[0].headshotImage!.content).toStrictEqual(asset.content) + expect(updatedIssuanceScenarioResult.personas[0].bodyImage).not.toBeNull() + expect(updatedIssuanceScenarioResult.personas[0].bodyImage!.id).toBeDefined() + expect(updatedIssuanceScenarioResult.personas[0].bodyImage!.mediaType).toEqual(asset.mediaType) + expect(updatedIssuanceScenarioResult.personas[0].bodyImage!.fileName).toEqual(asset.fileName) + expect(updatedIssuanceScenarioResult.personas[0].bodyImage!.description).toEqual(asset.description) + expect(updatedIssuanceScenarioResult.personas[0].bodyImage!.content).toStrictEqual(asset.content) + }) + + it('Should throw error when updating scenario with no steps', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedIssuanceScenario: NewIssuanceScenario = { + ...savedIssuanceScenario, + steps: [], + issuer: (savedIssuanceScenario).issuer!.id, + personas: [persona1.id], + bannerImage: null, + } + + await expect(repository.update(savedIssuanceScenario.id, updatedIssuanceScenario)).rejects.toThrowError( + `At least one step is required`, + ) + }) + + it('Should throw error when updating scenario with invalid issuer id', async (): Promise => { + const unknownIssuerId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedIssuanceScenario: NewIssuanceScenario = { + ...savedIssuanceScenario, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + issuer: unknownIssuerId, + personas: [persona1.id], + bannerImage: null, + } + + await expect(repository.update(savedIssuanceScenario.id, updatedIssuanceScenario)).rejects.toThrowError( + `No issuer found for id: ${unknownIssuerId}`, + ) + }) + + it('Should throw error when updating scenario with invalid issuer id', async (): Promise => { + const unknownIssuerId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedIssuanceScenario: NewIssuanceScenario = { + ...savedIssuanceScenario, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + issuer: unknownIssuerId, + personas: [persona1.id], + bannerImage: null, + } + + await expect(repository.update(savedIssuanceScenario.id, updatedIssuanceScenario)).rejects.toThrowError( + `No issuer found for id: ${unknownIssuerId}`, + ) + }) + + it('Should throw error when updating scenario with invalid banner image id', async (): Promise => { + const unknownBannerImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedIssuanceScenario: NewIssuanceScenario = { + ...savedIssuanceScenario, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + issuer: issuer.id, + personas: [persona1.id], + bannerImage: unknownBannerImageId, + } + + await expect(repository.update(savedIssuanceScenario.id, updatedIssuanceScenario)).rejects.toThrowError( + `No asset found for id: ${unknownBannerImageId}`, + ) + }) + + it('Should throw error when updating scenario with no personas', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedIssuanceScenario: NewIssuanceScenario = { + ...savedIssuanceScenario, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + issuer: (savedIssuanceScenario).issuer!.id, + personas: [], + bannerImage: null, + } + + await expect(repository.update(savedIssuanceScenario.id, updatedIssuanceScenario)).rejects.toThrowError( + `At least one persona is required`, + ) + }) + + it('Should add scenario step to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const step: NewStep = { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + // credentialDefinitionIdentifierType: credentialDefinition.identifierType, + // credentialDefinitionIdentifier: credentialDefinition.identifier, + actions: [ + { + title: 'example_title1', + actionType: StepActionType.ARIES_OOB, + text: 'example_text1', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + { + title: 'example_title2', + actionType: StepActionType.ARIES_OOB, + text: 'example_text2', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + } + const savedStep = await repository.createStep(savedIssuanceScenario.id, step) + expect(savedStep).toBeDefined() + + const fromDb = await repository.findById(savedIssuanceScenario.id) + expect(fromDb).toBeDefined() + + expect(fromDb.steps).toBeDefined() + expect(fromDb.steps.length).toEqual(2) + expect(fromDb.steps[1].title).toEqual(step.title) + expect(fromDb.steps[1].order).toEqual(step.order) + expect(fromDb.steps[1].type).toEqual(step.type) + expect(fromDb.steps[1].actions!.length).toEqual(2) + expect(fromDb.steps[1].actions![0].id).toBeDefined() + expect(fromDb.steps[1].actions![0].title).toEqual(step.actions![0].title) + expect(fromDb.steps[1].actions![0].actionType).toEqual(step.actions![0].actionType) + expect(fromDb.steps[1].actions![0].text).toEqual(step.actions![0].text) + expect(fromDb.steps[1].asset).not.toBeNull() + expect(fromDb.steps[1].asset!.mediaType).toEqual(asset.mediaType) + expect(fromDb.steps[1].asset!.fileName).toEqual(asset.fileName) + expect(fromDb.steps[1].asset!.description).toEqual(asset.description) + expect(fromDb.steps[1].asset!.content).toStrictEqual(asset.content) + if (fromDb.steps[0].actions![0].actionType === StepActionType.ARIES_OOB) { + const action = fromDb.steps[0].actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + }) + + it('Should not throw error when adding scenario step with no actions', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const step: NewStep = { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [], + } + + await expect(repository.createStep(savedIssuanceScenario.id, step)).resolves.toEqual({ + asset: { + content: expect.any(Buffer), + createdAt: expect.any(Date), + description: 'some image', + fileName: 'image.png', + id: expect.any(String), + mediaType: 'image/png', + updatedAt: expect.any(Date), + }, + createdAt: expect.any(Date), + description: 'example_description', + id: expect.any(String), + order: 2, + scenario: expect.any(String), + subScenario: null, + title: 'example_title', + type: 'HUMAN_TASK', + updatedAt: expect.any(Date), + }) + }) + + it('Should get scenario step by step id from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title1', + actionType: StepActionType.ARIES_OOB, + text: 'example_text1', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + { + title: 'example_title2', + actionType: StepActionType.ARIES_OOB, + text: 'example_text2', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const fromDb = await repository.findByStepId(savedIssuanceScenario.id, savedIssuanceScenario.steps[0].id) + + expect(fromDb).toBeDefined() + expect(fromDb.id).toEqual(savedIssuanceScenario.steps[0].id) + expect(fromDb.title).toEqual(issuanceScenario.steps[0].title) + expect(fromDb.order).toEqual(issuanceScenario.steps[0].order) + expect(fromDb.type).toEqual(issuanceScenario.steps[0].type) + expect(fromDb.actions!.length).toEqual(2) + expect(fromDb.actions![0].id).toBeDefined() + expect(fromDb.actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(fromDb.actions![0].actionType).toEqual(issuanceScenario.steps[0].actions![0].actionType) + expect(fromDb.actions![0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + expect(fromDb.asset).not.toBeNull() + expect(fromDb.asset!.mediaType).toEqual(asset.mediaType) + expect(fromDb.asset!.fileName).toEqual(asset.fileName) + expect(fromDb.asset!.description).toEqual(asset.description) + expect(fromDb.asset!.content).toStrictEqual(asset.content) + if (fromDb.actions![0].actionType === StepActionType.ARIES_OOB) { + const action = fromDb.actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + }) + + it('Should get all scenario steps from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const fromDb = await repository.findAllSteps(savedIssuanceScenario.id) + + expect(fromDb).toBeDefined() + expect(fromDb.length).toEqual(2) + expect(fromDb[0].id).toBeDefined() + expect(fromDb[0].title).toEqual(issuanceScenario.steps[0].title) + expect(fromDb[0].order).toEqual(issuanceScenario.steps[0].order) + expect(fromDb[0].type).toEqual(issuanceScenario.steps[0].type) + expect(fromDb[0].actions!.length).toEqual(1) + expect(fromDb[0].actions![0].id).toBeDefined() + expect(fromDb[0].actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(fromDb[0].actions![0].actionType).toEqual(issuanceScenario.steps[0].actions![0].actionType) + expect(fromDb[0].actions![0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + expect(fromDb[0].asset).not.toBeNull() + expect(fromDb[0].asset!.mediaType).toEqual(asset.mediaType) + expect(fromDb[0].asset!.fileName).toEqual(asset.fileName) + expect(fromDb[0].asset!.description).toEqual(asset.description) + expect(fromDb[0].asset!.content).toStrictEqual(asset.content) + }) + + it('Should delete scenario step from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + { + title: 'example_title', + description: 'example_description', + order: 2, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.steps).toBeDefined() + expect(savedIssuanceScenario.steps.length).toEqual(2) + + await repository.deleteStep(savedIssuanceScenario.id, savedIssuanceScenario.steps[1].id) + const fromDb = await repository.findById(savedIssuanceScenario.id) + + expect(fromDb.steps).toBeDefined() + expect(fromDb.steps.length).toEqual(1) + }) + + it('Should update scenario step in database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + // credentialDefinitionIdentifierType: credentialDefinition.identifierType, + // credentialDefinitionIdentifier: credentialDefinition.identifier, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedStep: NewStep = { + ...savedIssuanceScenario.steps[0], + title: 'new_title', + // credentialDefinitionIdentifierType: credentialDefinition.identifierType, + // credentialDefinitionIdentifier: credentialDefinition.identifier, + actions: [ + { + title: 'example_title1', + actionType: StepActionType.ARIES_OOB, + text: 'example_text1', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + { + title: 'example_title2', + actionType: StepActionType.ARIES_OOB, + text: 'example_text2', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + asset: savedIssuanceScenario.steps[0].asset!.id, + } + const updatedStepResult = await repository.updateStep( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + updatedStep, + ) + + expect(updatedStepResult).toBeDefined() + expect(updatedStepResult.title).toEqual(updatedStep.title) + expect(updatedStepResult.order).toEqual(updatedStep.order) + expect(updatedStepResult.type).toEqual(updatedStep.type) + expect(updatedStepResult.actions!.length).toEqual(2) + expect(updatedStepResult.actions![0].id).toBeDefined() + expect(updatedStepResult.actions![0].title).toEqual(updatedStep.actions![0].title) + expect(updatedStepResult.actions![0].actionType).toEqual(updatedStep.actions![0].actionType) + expect(updatedStepResult.actions![0].text).toEqual(updatedStep.actions![0].text) + expect(updatedStepResult.asset).not.toBeNull() + expect(updatedStepResult.asset!.mediaType).toEqual(asset.mediaType) + expect(updatedStepResult.asset!.fileName).toEqual(asset.fileName) + expect(updatedStepResult.asset!.description).toEqual(asset.description) + expect(updatedStepResult.asset!.content).toStrictEqual(asset.content) + if (updatedStepResult.actions![0].actionType === StepActionType.ARIES_OOB) { + const action = updatedStepResult.actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + }) + + it('Should not throw error when updating scenario step with no actions', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedStep: NewStep = { + ...savedIssuanceScenario.steps[0], + actions: [], + asset: savedIssuanceScenario.steps[0].asset!.id, + } + + await expect( + repository.updateStep(savedIssuanceScenario.id, savedIssuanceScenario.steps[0].id, updatedStep), + ).resolves.toEqual({ + asset: { + content: expect.any(Buffer), + createdAt: expect.any(Date), + description: 'some image', + fileName: 'image.png', + id: expect.any(String), + mediaType: 'image/png', + updatedAt: expect.any(Date), + }, + createdAt: expect.any(Date), + description: 'example_description', + id: expect.any(String), + order: 1, + scenario: expect.any(String), + subScenario: null, + title: 'example_title', + type: 'HUMAN_TASK', + updatedAt: expect.any(Date), + }) + }) + + it('Should add to scenario step action to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const action: NewAriesOOBAction = { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + } + const savedStepAction = await repository.createStepAction( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + action, + ) + expect(savedStepAction).toBeDefined() + + const fromDb = await repository.findById(savedIssuanceScenario.id) + expect(fromDb).toBeDefined() + + expect(fromDb.steps).toBeDefined() + expect(fromDb.steps.length).toEqual(1) + expect(fromDb.steps[0].actions).toBeDefined() + expect(fromDb.steps[0].actions!.length).toEqual(2) + expect(fromDb.steps[0].actions![1].id).toBeDefined() + expect(fromDb.steps[0].actions![1].title).toEqual(action.title) + expect(fromDb.steps[0].actions![1].actionType).toEqual(action.actionType) + expect(fromDb.steps[0].actions![1].text).toEqual(action.text) + if (fromDb.steps[0].actions![0].actionType === StepActionType.ARIES_OOB) { + const action = fromDb.steps[0].actions![0] as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + }) + + it('Should get scenario step action by action id from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const fromDb = await repository.findByStepActionId( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + savedIssuanceScenario.steps[0].actions![0].id, + ) + + expect(fromDb.id).toEqual(savedIssuanceScenario.steps[0].actions![0].id) + expect(fromDb.title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(fromDb.actionType).toEqual(issuanceScenario.steps[0].actions![0].actionType) + expect(fromDb.text).toEqual(issuanceScenario.steps[0].actions![0].text) + if (fromDb.actionType === StepActionType.ARIES_OOB) { + const action = fromDb as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + }) + + it('Should get all scenario step actions from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const fromDb = await repository.findAllStepActions(savedIssuanceScenario.id, savedIssuanceScenario.steps[0].id) + + expect(fromDb).toBeDefined() + expect(fromDb.length).toEqual(2) + expect(fromDb[0].id).toBeDefined() + expect(fromDb[0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(fromDb[0].actionType).toEqual(issuanceScenario.steps[0].actions![0].actionType) + expect(fromDb[0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + }) + + it('Should delete scenario step action from database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.steps[0].actions!).toBeDefined() + expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(2) + + await repository.deleteStepAction( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + savedIssuanceScenario.steps[0].actions![1].id, + ) + const fromDb = await repository.findById(savedIssuanceScenario.id) + + expect(fromDb.steps[0].actions).toBeDefined() + expect(fromDb.steps[0].actions!.length).toEqual(1) + }) + + it('Should update scenario step action in database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedStepAction: NewAriesOOBAction = { + title: 'new_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + attribute2: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: { + predicate1: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + predicate2: { + name: 'example_name', + type: '>=', + value: 1, + restrictions: ['restriction1', 'restriction2'], + }, + }, + }, + } + const updatedStepResult = await repository.updateStepAction( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + savedIssuanceScenario.steps[0].actions![0].id, + updatedStepAction, + ) + + expect(updatedStepResult).toBeDefined() + expect(updatedStepResult.id).toBeDefined() + expect(updatedStepResult.title).toEqual(updatedStepAction.title) + expect(updatedStepResult.actionType).toEqual(updatedStepAction.actionType) + expect(updatedStepResult.text).toEqual(updatedStepAction.text) + + if (updatedStepResult.actionType === StepActionType.ARIES_OOB) { + const action = updatedStepResult as AriesOOBAction + expect(action.proofRequest).toBeDefined() + + if (action.proofRequest) { + expect(action.proofRequest.attributes).toBeDefined() + expect(action.proofRequest.attributes!.attribute1).toBeDefined() + expect(action.proofRequest.attributes!.attribute1.attributes?.length).toEqual(2) + expect(action.proofRequest.attributes!.attribute1.restrictions?.length).toEqual(2) + + expect(action.proofRequest.predicates).toBeDefined() + expect(action.proofRequest.predicates!.predicate1).toBeDefined() + expect(action.proofRequest.predicates!.predicate1.restrictions?.length).toEqual(2) + } + } + }) + + it('Should save scenario with ButtonAction to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.BUTTON, + text: 'example_text', + goToStep: 'step1', + } as NewButtonAction, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.steps).toBeDefined() + expect(savedIssuanceScenario.steps.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(1) + const buttonAction = savedIssuanceScenario.steps[0].actions![0] as ButtonAction + expect(buttonAction.actionType).toEqual(StepActionType.BUTTON) + expect(buttonAction.title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(buttonAction.text).toEqual(issuanceScenario.steps[0].actions![0].text) + expect(buttonAction.goToStep).toEqual('step1') + }) + + it('Should save scenario with SetupConnectionAction to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.SETUP_CONNECTION, + text: 'example_text', + } as NewSetupConnectionAction, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.steps).toBeDefined() + expect(savedIssuanceScenario.steps.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions![0].actionType).toEqual(StepActionType.SETUP_CONNECTION) + expect(savedIssuanceScenario.steps[0].actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(savedIssuanceScenario.steps[0].actions![0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + }) + + it('Should save scenario with ChooseWalletAction to database', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.CHOOSE_WALLET, + text: 'example_text', + } as NewChooseWalletAction, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + + expect(savedIssuanceScenario).toBeDefined() + expect(savedIssuanceScenario.steps).toBeDefined() + expect(savedIssuanceScenario.steps.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(1) + expect(savedIssuanceScenario.steps[0].actions![0].actionType).toEqual(StepActionType.CHOOSE_WALLET) + expect(savedIssuanceScenario.steps[0].actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) + expect(savedIssuanceScenario.steps[0].actions![0].text).toEqual(issuanceScenario.steps[0].actions![0].text) + }) + + it('Should correctly update a step with multiple action types', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: {}, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + const updatedStep: NewStep = { + title: 'updated_title', + description: 'updated_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'button_title', + actionType: StepActionType.BUTTON, + text: 'button_text', + goToStep: 'step2', + } as NewButtonAction, + { + title: 'setup_title', + actionType: StepActionType.SETUP_CONNECTION, + text: 'setup_text', + } as NewSetupConnectionAction, + { + title: 'wallet_title', + actionType: StepActionType.CHOOSE_WALLET, + text: 'wallet_text', + } as NewChooseWalletAction, + ], + } + + const updatedStepResult = await repository.updateStep( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + updatedStep, + ) + + expect(updatedStepResult).toBeDefined() + expect(updatedStepResult.title).toEqual(updatedStep.title) + expect(updatedStepResult.actions!.length).toEqual(3) + + // Check button action + const buttonAction = updatedStepResult.actions!.find((a) => a.actionType === StepActionType.BUTTON) as ButtonAction + expect(buttonAction).toBeDefined() + expect(buttonAction?.title).toEqual('button_title') + expect(buttonAction?.text).toEqual('button_text') + expect(buttonAction?.goToStep).toEqual('step2') + + // Check setup connection action + const setupAction = updatedStepResult.actions!.find((a) => a.actionType === StepActionType.SETUP_CONNECTION) + expect(setupAction).toBeDefined() + expect(setupAction?.title).toEqual('setup_title') + expect(setupAction?.text).toEqual('setup_text') + + // Check choose wallet action + const walletAction = updatedStepResult.actions!.find((a) => a.actionType === StepActionType.CHOOSE_WALLET) + expect(walletAction).toBeDefined() + expect(walletAction?.title).toEqual('wallet_title') + expect(walletAction?.text).toEqual('wallet_text') + }) + + it('Should create individual step actions of different types', async (): Promise => { + const issuanceScenario: NewIssuanceScenario = { + name: 'example_name', + description: 'example_description', + issuer: issuer.id, + steps: [ + { + title: 'example_title', + description: 'example_description', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'example_title', + actionType: StepActionType.ARIES_OOB, + text: 'example_text', + proofRequest: { + attributes: {}, + predicates: {}, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedIssuanceScenario = await repository.create(issuanceScenario) + expect(savedIssuanceScenario).toBeDefined() + + // Add button action + const buttonAction: NewButtonAction = { + title: 'button_title', + actionType: StepActionType.BUTTON, + text: 'button_text', + goToStep: 'next_step', + } + + const savedButtonAction = (await repository.createStepAction( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + buttonAction, + )) as ButtonAction + + expect(savedButtonAction).toBeDefined() + expect(savedButtonAction.actionType).toEqual(StepActionType.BUTTON) + expect(savedButtonAction.title).toEqual(buttonAction.title) + expect(savedButtonAction.text).toEqual(buttonAction.text) + expect(savedButtonAction.goToStep).toEqual(buttonAction.goToStep) + + // Add setup connection action + const setupAction: NewSetupConnectionAction = { + title: 'setup_title', + actionType: StepActionType.SETUP_CONNECTION, + text: 'setup_text', + } + + const savedSetupAction = await repository.createStepAction( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + setupAction, + ) + + expect(savedSetupAction).toBeDefined() + expect(savedSetupAction.actionType).toEqual(StepActionType.SETUP_CONNECTION) + expect(savedSetupAction.title).toEqual(setupAction.title) + expect(savedSetupAction.text).toEqual(setupAction.text) + + // Add choose wallet action + const walletAction: NewChooseWalletAction = { + title: 'wallet_title', + actionType: StepActionType.CHOOSE_WALLET, + text: 'wallet_text', + } + + const savedWalletAction = await repository.createStepAction( + savedIssuanceScenario.id, + savedIssuanceScenario.steps[0].id, + walletAction, + ) + + expect(savedWalletAction).toBeDefined() + expect(savedWalletAction.actionType).toEqual(StepActionType.CHOOSE_WALLET) + expect(savedWalletAction.title).toEqual(walletAction.title) + expect(savedWalletAction.text).toEqual(walletAction.text) + + // Check all actions are in the step + const updatedStep = await repository.findByStepId(savedIssuanceScenario.id, savedIssuanceScenario.steps[0].id) + expect(updatedStep.actions?.length).toEqual(4) // Original + 3 new ones + }) + + it('Should correctly retrieve presentation scenario with relyingParty details', async (): Promise => { + const presentationScenario: NewPresentationScenario = { + name: 'presentation_scenario', + description: 'presentation scenario description', + relyingParty: relyingParty.id, + steps: [ + { + title: 'verification step', + description: 'verify credentials', + order: 1, + type: StepType.HUMAN_TASK, + asset: asset.id, + actions: [ + { + title: 'verify action', + actionType: StepActionType.ARIES_OOB, + text: 'verify your credentials', + proofRequest: { + attributes: { + attribute1: { + attributes: ['attribute1', 'attribute2'], + restrictions: ['restriction1', 'restriction2'], + }, + }, + predicates: {}, + }, + }, + ], + }, + ], + personas: [persona1.id], + hidden: false, + } + + const savedScenario = await repository.create(presentationScenario) + const retrievedScenario = await repository.findById(savedScenario.id) + + expect(retrievedScenario).toBeDefined() + expect(retrievedScenario.scenarioType).toEqual(ScenarioType.PRESENTATION) + + // Check relyingParty details + const presentationResult = retrievedScenario as PresentationScenario + expect(presentationResult.relyingParty).toBeDefined() + expect(presentationResult.relyingParty?.id).toEqual(relyingParty.id) + expect(presentationResult.relyingParty?.name).toEqual(relyingParty.name) + expect(presentationResult.relyingParty?.type).toEqual(relyingParty.type) + expect(presentationResult.relyingParty?.description).toEqual(relyingParty.description) + expect(presentationResult.relyingParty?.organization).toEqual(relyingParty.organization) + + // Check relyingParty assets + expect(presentationResult.relyingParty?.logo).toBeDefined() + expect(presentationResult.relyingParty?.logo?.id).toEqual(asset.id) + expect(presentationResult.relyingParty?.logo?.mediaType).toEqual(asset.mediaType) + expect(presentationResult.relyingParty?.logo?.fileName).toEqual(asset.fileName) + + // Check relyingParty credential definitions + expect(presentationResult.relyingParty?.credentialDefinitions).toBeDefined() + expect(presentationResult.relyingParty?.credentialDefinitions.length).toEqual(1) + expect(presentationResult.relyingParty?.credentialDefinitions[0].name).toBeDefined() + expect(presentationResult.relyingParty?.credentialDefinitions[0].icon).toBeDefined() + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/showcase.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/showcase.repository.test.ts new file mode 100644 index 000000000..7aa1bf023 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/showcase.repository.test.ts @@ -0,0 +1,377 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { + Asset, + CredentialDefinition, + IssuanceScenario, + NewShowcase, + Persona, + ShowcaseStatus, + Tenant, + User, +} from '../../../types' +import * as schema from '../../schema' +import ShowcaseRepository from '../ShowcaseRepository' +import { + createTestAsset, + createTestCredentialDefinition, + createTestCredentialSchema, + createTestIssuer, + createTestPersona, + createTestScenario, + createTestTenant, + createTestUser, +} from './dbTestData' + +describe('Database showcase repository tests', (): void => { + let client: PGlite + let repository: ShowcaseRepository + let persona1: Persona + let persona2: Persona + let issuanceScenario1: IssuanceScenario + let issuanceScenario2: IssuanceScenario + let credentialDefinition1: CredentialDefinition + let credentialDefinition2: CredentialDefinition + let asset: Asset + let user: User + let tenant: Tenant + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + repository = Container.get(ShowcaseRepository) + + // Create test utilities using dbTestData helper functions + user = await createTestUser('test-user-showcase') + asset = await createTestAsset() + tenant = await createTestTenant() + + // Create credential schema and definitions + const credentialSchema = await createTestCredentialSchema() + credentialDefinition1 = await createTestCredentialDefinition(asset, credentialSchema, tenant.id) + credentialDefinition2 = await createTestCredentialDefinition(asset, credentialSchema, tenant.id) + + // Create issuer + const issuer = await createTestIssuer(asset, credentialDefinition1, credentialSchema, tenant.id) + + // Create personas + persona1 = await createTestPersona(asset) + persona2 = await createTestPersona(asset) + + // Create scenarios + issuanceScenario1 = await createTestScenario(asset, persona1, issuer, credentialDefinition1.id, tenant.id) + issuanceScenario2 = await createTestScenario(asset, persona2, issuer, credentialDefinition2.id, tenant.id) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save showcase to database', async (): Promise => { + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + bannerImage: asset.id, + createdBy: user.id, + } + + const savedShowcase = await repository.create(showcase) + + expect(savedShowcase).toBeDefined() + expect(savedShowcase.name).toEqual(showcase.name) + expect(savedShowcase.slug).toEqual('example-name') + expect(savedShowcase.description).toEqual(showcase.description) + expect(savedShowcase.status).toEqual(showcase.status) + expect(savedShowcase.hidden).toEqual(showcase.hidden) + expect(savedShowcase.scenarios.length).toEqual(2) + expect(savedShowcase.personas.length).toEqual(2) + expect(savedShowcase.bannerImage!.id).toBeDefined() + expect(savedShowcase.bannerImage!.mediaType).toEqual(asset.mediaType) + expect(savedShowcase.bannerImage!.fileName).toEqual(asset.fileName) + expect(savedShowcase.bannerImage!.description).toEqual(asset.description) + expect(savedShowcase.bannerImage!.content).toStrictEqual(asset.content) + expect(savedShowcase.createdBy).toEqual({ + userName: 'test-user-showcase', + createdAt: expect.any(Date), + id: expect.any(String), + updatedAt: expect.any(Date), + issuer: 'https://auth-server.example.com/auth/realms/BC', + tenants: [], + clientId: 'showcase-tenantA', + }) + }) + + it('Should throw error when saving showcase with invalid persona id', async (): Promise => { + const unknownPersonaId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [unknownPersonaId], + } + + await expect(repository.create(showcase)).rejects.toThrowError(`No persona found for id: ${unknownPersonaId}`) + }) + + it('Should throw error when saving showcase with invalid banner image id', async (): Promise => { + const unknownBannerImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + bannerImage: unknownBannerImageId, + } + + await expect(repository.create(showcase)).rejects.toThrowError(`No asset found for id: ${unknownBannerImageId}`) + }) + + it('Should throw error when saving showcase with invalid scenario id', async (): Promise => { + const unknownScenarioId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [unknownScenarioId], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + await expect(repository.create(showcase)).rejects.toThrowError(`No scenario found for id: ${unknownScenarioId}`) + }) + + it('Should get showcase by id from database', async (): Promise => { + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase = await repository.create(showcase) + expect(savedShowcase).toBeDefined() + + const fromDb = await repository.findById(savedShowcase.id) + + expect(fromDb).toBeDefined() + expect(fromDb.name).toEqual(showcase.name) + expect(fromDb.description).toEqual(showcase.description) + expect(fromDb.status).toEqual(showcase.status) + expect(fromDb.hidden).toEqual(showcase.hidden) + expect(fromDb.scenarios.length).toEqual(2) + expect(fromDb.personas.length).toEqual(2) + }) + + it('Should get all showcases from database', async (): Promise => { + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase1 = await repository.create(showcase) + expect(savedShowcase1).toBeDefined() + + const savedShowcase2 = await repository.create(showcase) + expect(savedShowcase2).toBeDefined() + + const fromDb = await repository.findAll(tenant.id) + + expect(fromDb).toBeDefined() + expect(fromDb.length).toEqual(2) + }) + + it('Should delete showcase from database', async (): Promise => { + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase = await repository.create(showcase) + expect(savedShowcase).toBeDefined() + + await repository.delete(savedShowcase.id) + + await expect(repository.findById(savedShowcase.id)).rejects.toThrowError( + `No showcase found for id: ${savedShowcase.id}`, + ) + }) + + it('Should update showcase in database', async (): Promise => { + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase = await repository.create(showcase) + + const newName = 'new_name' + const completionMessage = 'showcase completed' + const updatedShowcase = await repository.update(savedShowcase.id, { + name: newName, + description: savedShowcase.description, + status: savedShowcase.status, + hidden: savedShowcase.hidden, + tenantId: savedShowcase.tenantId, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + personas: [persona1.id, persona2.id], + bannerImage: null, + completionMessage, + }) + + expect(updatedShowcase).toBeDefined() + expect(updatedShowcase.name).toEqual(newName) + expect(updatedShowcase.slug).toEqual('new-name') + expect(updatedShowcase.completionMessage).toEqual(completionMessage) + expect(updatedShowcase.description).toEqual(showcase.description) + expect(updatedShowcase.status).toEqual(showcase.status) + expect(updatedShowcase.hidden).toEqual(showcase.hidden) + expect(updatedShowcase.scenarios.length).toEqual(2) + expect(updatedShowcase.personas.length).toEqual(2) + }) + + it('Should throw error when updating showcase with invalid persona id', async (): Promise => { + const unknownPersonaId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase = await repository.create(showcase) + + const updatedShowcase: NewShowcase = { + name: savedShowcase.name, + description: savedShowcase.description, + status: savedShowcase.status, + hidden: savedShowcase.hidden, + tenantId: savedShowcase.tenantId, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [unknownPersonaId], + bannerImage: null, + } + + await expect(repository.update(savedShowcase.id, updatedShowcase)).rejects.toThrowError( + `No persona found for id: ${unknownPersonaId}`, + ) + }) + + it('Should throw error when updating showcase with invalid scenario id', async (): Promise => { + const unknownScenarioId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase = await repository.create(showcase) + + const updatedShowcase: NewShowcase = { + name: savedShowcase.name, + description: savedShowcase.description, + status: savedShowcase.status, + hidden: savedShowcase.hidden, + tenantId: savedShowcase.tenantId, + scenarios: [unknownScenarioId], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + bannerImage: null, + } + + await expect(repository.update(savedShowcase.id, updatedShowcase)).rejects.toThrowError( + `No scenario found for id: ${unknownScenarioId}`, + ) + }) + + it('Should throw error when updating showcase with invalid banner image id', async (): Promise => { + const unknownBannerImageId = 'a197e5b2-e4e5-4788-83b1-ecaa0e99ed3a' + const showcase: NewShowcase = { + name: 'example_name', + description: 'example_description', + status: ShowcaseStatus.ACTIVE, + hidden: false, + tenantId: tenant.id, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + } + + const savedShowcase = await repository.create(showcase) + + const updatedShowcase: NewShowcase = { + name: savedShowcase.name, + description: savedShowcase.description, + status: savedShowcase.status, + hidden: savedShowcase.hidden, + tenantId: savedShowcase.tenantId, + scenarios: [issuanceScenario1.id, issuanceScenario2.id], + credentialDefinitions: [credentialDefinition1.id, credentialDefinition2.id], + personas: [persona1.id, persona2.id], + bannerImage: unknownBannerImageId, + } + await expect(repository.update(savedShowcase.id, updatedShowcase)).rejects.toThrowError( + `No asset found for id: ${unknownBannerImageId}`, + ) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/tenant.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/tenant.repository.test.ts new file mode 100644 index 000000000..7c31f0463 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/tenant.repository.test.ts @@ -0,0 +1,345 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { NewTenant, NewUser, TenantType } from '../../../types' +import * as schema from '../../schema' +import TenantRepository from '../TenantRepository' +import UserRepository from '../UserRepository' + +describe('Database tenant repository tests', (): void => { + let client: PGlite + let tenantRepository: TenantRepository + let userRepository: UserRepository + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + Container.set({ id: UserRepository, type: UserRepository }) + userRepository = Container.get(UserRepository) + tenantRepository = Container.get(TenantRepository) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save tenant to database', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + + expect(savedTenant).toBeDefined() + expect(savedTenant.id).toEqual(tenant.id) + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + }) + + it('Should get tenant by id from database', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + + const fromDb = await tenantRepository.findById(savedTenant.id) + + expect(fromDb).toBeDefined() + expect(fromDb.id).toEqual(tenant.id) + }) + + it('Should get all tenants from database', async (): Promise => { + const tenant1: NewTenant = { + id: 'test-tenant-id-1', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const tenant2: NewTenant = { + id: 'test-tenant-id-2', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant1 = await tenantRepository.create(tenant1) + expect(savedTenant1).toBeDefined() + + const savedTenant2 = await tenantRepository.create(tenant2) + expect(savedTenant2).toBeDefined() + + const fromDb = await tenantRepository.findAll() + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete tenant from database', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + + await tenantRepository.delete(savedTenant.id) + + await expect(tenantRepository.findById(savedTenant.id)).rejects.toThrowError( + `No tenant found for id: ${savedTenant.id}`, + ) + }) + + it('Should update tenant in database', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + + const newTenantId = 'updated-test-tenant-id' + const updatedTenant = await tenantRepository.update(savedTenant.id, { + id: newTenantId, + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + }) + + expect(updatedTenant).toBeDefined() + expect(updatedTenant.id).toEqual(newTenantId) + }) + + it('Should save tenant with users to database', async (): Promise => { + const user1: NewUser = { id: '550e8400-e29b-41d4-a716-446655440000', userName: 'User 1' } + const user2: NewUser = { id: '550e8400-e29b-41d4-a716-446655440001', userName: 'User 2' } + + await userRepository.create(user1) + await userRepository.create(user2) + + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + users: [user1.id!, user2.id!], + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + + expect(savedTenant).toBeDefined() + expect(savedTenant.id).toEqual(tenant.id) + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + expect(savedTenant.users).toHaveLength(2) + expect(savedTenant.users).toEqual([ + { + clientId: null, + createdAt: expect.any(Date), + id: '550e8400-e29b-41d4-a716-446655440000', + issuer: null, + updatedAt: expect.any(Date), + userName: 'User 1', + }, + { + clientId: null, + createdAt: expect.any(Date), + id: '550e8400-e29b-41d4-a716-446655440001', + issuer: null, + updatedAt: expect.any(Date), + userName: 'User 2', + }, + ]) + }) + + it('Should update tenant with users in database', async (): Promise => { + const user1: NewUser = { id: '550e8400-e29b-41d4-a716-446655440000', userName: 'User 1' } + const user2: NewUser = { id: '550e8400-e29b-41d4-a716-446655440001', userName: 'User 2' } + const user3: NewUser = { id: 'c83e7b9817fe4d43aedcc071f64f0bf6', userName: 'User 3' } + + await userRepository.create(user1) + await userRepository.create(user2) + await userRepository.create(user3) + + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + users: [user1.id!, user2.id!], + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + + const updatedTenant = await tenantRepository.update(savedTenant.id, { + id: savedTenant.id, + tenantType: TenantType.SHOWCASE, + users: [user3.id!], + oidcIssuer: 'https://auth-server/auth/realms/test', + }) + + expect(updatedTenant).toBeDefined() + expect(updatedTenant.id).toEqual(savedTenant.id) + expect(updatedTenant.users).toHaveLength(1) + expect(updatedTenant.users).toEqual([ + { + clientId: null, + createdAt: expect.any(Date), + id: 'c83e7b98-17fe-4d43-aedc-c071f64f0bf6', + issuer: null, + updatedAt: expect.any(Date), + userName: 'User 3', + }, + ]) + }) + + it('Should save tenant with Traction fields to database', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + tractionTenantId: '550e8400-e29b-41d4-a716-446655440000', + tractionWalletId: '550e8400-e29b-41d4-a716-446655440001', + tractionApiUrl: 'https://api.traction.example.com', + } + + const savedTenant = await tenantRepository.create(tenant) + + expect(savedTenant).toBeDefined() + expect(savedTenant.id).toEqual(tenant.id) + expect(savedTenant.tractionTenantId).toEqual(tenant.tractionTenantId) + expect(savedTenant.tractionWalletId).toEqual(tenant.tractionWalletId) + expect(savedTenant.tractionApiUrl).toEqual(tenant.tractionApiUrl) + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + }) + + it('Should update Traction fields in tenant', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + expect(savedTenant.tractionTenantId).toBeNull() + expect(savedTenant.tractionWalletId).toBeNull() + expect(savedTenant.tractionApiUrl).toBeNull() + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + + const updatedTenant = await tenantRepository.update(savedTenant.id, { + id: savedTenant.id, + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + tractionTenantId: '550e8400-e29b-41d4-a716-446655440000', + tractionWalletId: '550e8400-e29b-41d4-a716-446655440001', + tractionApiUrl: 'https://api.traction.example.com', + }) + + expect(updatedTenant).toBeDefined() + expect(updatedTenant.id).toEqual(savedTenant.id) + expect(updatedTenant.tractionTenantId).toEqual('550e8400-e29b-41d4-a716-446655440000') + expect(updatedTenant.tractionWalletId).toEqual('550e8400-e29b-41d4-a716-446655440001') + expect(updatedTenant.tractionApiUrl).toEqual('https://api.traction.example.com') + }) + + it('Should be able to update tenant with different tenantType', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + expect(savedTenant.tenantType).toEqual(TenantType.SHOWCASE) + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + + const updatedTenant = await tenantRepository.update(savedTenant.id, { + id: savedTenant.id, + tenantType: TenantType.ROOT, + oidcIssuer: 'https://auth-server/auth/realms/test', + }) + + expect(updatedTenant).toBeDefined() + expect(updatedTenant.tenantType).toEqual(TenantType.ROOT) + }) + + it('Should save tenant with all fields to database', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + tractionTenantId: '550e8400-e29b-41d4-a716-446655440000', + tractionWalletId: '550e8400-e29b-41d4-a716-446655440001', + tractionApiUrl: 'https://api.traction.example.com', + tractionApiKey: 'c83e7b9817fe4d43aedcc071f64f0bf6', + nonceBase64: 'base64EncodedNonceString', + } + + const savedTenant = await tenantRepository.create(tenant) + + expect(savedTenant).toBeDefined() + expect(savedTenant.id).toEqual(tenant.id) + expect(savedTenant.tractionTenantId).toEqual(tenant.tractionTenantId) + expect(savedTenant.tractionWalletId).toEqual(tenant.tractionWalletId) + expect(savedTenant.tractionApiUrl).toEqual(tenant.tractionApiUrl) + expect(savedTenant.tractionApiKey).toEqual(tenant.tractionApiKey) + expect(savedTenant.nonceBase64).toEqual(tenant.nonceBase64) + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + }) + + it('Should update tractionApiKey and nonceBase64 fields in tenant', async (): Promise => { + const tenant: NewTenant = { + id: 'test-tenant-id', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + expect(savedTenant.tractionApiKey).toBeNull() + expect(savedTenant.nonceBase64).toBeNull() + expect(savedTenant.issuers).toEqual([]) + expect(savedTenant.relyingParties).toEqual([]) + + const updatedTenant = await tenantRepository.update(savedTenant.id, { + id: savedTenant.id, + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + tractionApiKey: 'c83e7b9817fe4d43aedcc071f64f0bf6', + nonceBase64: 'base64EncodedNonceString', + }) + + expect(updatedTenant).toBeDefined() + expect(updatedTenant.id).toEqual(savedTenant.id) + expect(updatedTenant.tractionApiKey).toEqual('c83e7b9817fe4d43aedcc071f64f0bf6') + expect(updatedTenant.nonceBase64).toEqual('base64EncodedNonceString') + expect(updatedTenant.issuers).toEqual([]) + expect(updatedTenant.relyingParties).toEqual([]) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/repositories/__tests__/user.repository.test.ts b/apps/bc-wallet-api-server/src/database/repositories/__tests__/user.repository.test.ts new file mode 100644 index 000000000..276cf9399 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/repositories/__tests__/user.repository.test.ts @@ -0,0 +1,214 @@ +import 'reflect-metadata' +import { PGlite } from '@electric-sql/pglite' +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { drizzle } from 'drizzle-orm/pglite' +import { Container } from 'typedi' + +import DatabaseService from '../../../services/DatabaseService' +import { NewTenant, NewUser, TenantType } from '../../../types' +import * as schema from '../../schema' +import TenantRepository from '../TenantRepository' +import UserRepository from '../UserRepository' + +describe('Database user repository tests', (): void => { + let client: PGlite + let userRepository: UserRepository + let tenantRepository: TenantRepository + + beforeEach(async (): Promise => { + client = new PGlite() + const database = drizzle(client, { schema }) as unknown as NodePgDatabase + await migrate(database, { migrationsFolder: './apps/bc-wallet-api-server/src/database/migrations' }) + const mockDatabaseService = { + getConnection: jest.fn().mockResolvedValue(database), + } + Container.set(DatabaseService, mockDatabaseService) + tenantRepository = Container.get(TenantRepository) + userRepository = Container.get(UserRepository) + }) + + afterEach(async (): Promise => { + await client.close() + jest.resetAllMocks() + Container.reset() + }) + + it('Should save user to database', async (): Promise => { + const user: NewUser = { + userName: 'test_user', + issuer: 'https://auth-server.example.com/auth/realms/BC', // from iss claim + clientId: 'showcase-tenantA', // from azp claim + } + + const savedUser = await userRepository.create(user) + + expect(savedUser).toBeDefined() + expect(savedUser.userName).toEqual(user.userName) + expect(savedUser.issuer).toEqual(user.issuer) + expect(savedUser.clientId).toEqual(user.clientId) + }) + + it('Should get user by id from database', async (): Promise => { + const user: NewUser = { + userName: 'test_user', + issuer: 'https://auth-server.example.com/auth/realms/BC', + clientId: 'showcase-tenantA', + } + + const savedUser = await userRepository.create(user) + expect(savedUser).toBeDefined() + + const fromDb = await userRepository.findById(savedUser.id) + + expect(fromDb).toBeDefined() + expect(fromDb!.userName).toEqual(user.userName) + expect(fromDb!.issuer).toEqual(user.issuer) + expect(fromDb!.clientId).toEqual(user.clientId) + }) + + it('Should get all users from database', async (): Promise => { + const user: NewUser = { + userName: 'test_user', + issuer: 'https://auth-server.example.com/auth/realms/BC', + clientId: 'showcase-tenantA', + } + + const savedUser1 = await userRepository.create(user) + expect(savedUser1).toBeDefined() + + const savedUser2 = await userRepository.create(user) + expect(savedUser2).toBeDefined() + + const fromDb = await userRepository.findAll() + + expect(fromDb.length).toEqual(2) + }) + + it('Should delete user from database', async (): Promise => { + const user: NewUser = { + userName: 'test_user', + issuer: 'https://auth-server.example.com/auth/realms/BC', + clientId: 'showcase-tenantA', + } + + const savedUser = await userRepository.create(user) + expect(savedUser).toBeDefined() + + await userRepository.delete(savedUser.id) + + await expect(userRepository.findById(savedUser.id)).rejects.toThrowError(`No user found for id: ${savedUser.id}`) + }) + + it('Should update user in database', async (): Promise => { + const user: NewUser = { + userName: 'test_user', + issuer: 'https://auth-server.example.com/auth/realms/BC', + clientId: 'showcase-tenantA', + } + + const savedUser = await userRepository.create(user) + expect(savedUser).toBeDefined() + + const newClientId = 'showcase-tenantB' + const updatedUser = await userRepository.update(savedUser.id, { + ...savedUser, + clientId: newClientId, + tenants: undefined, + }) + + expect(updatedUser).toBeDefined() + expect(updatedUser.clientId).toEqual(newClientId) + }) + + it('Should save user with tenants to database', async (): Promise => { + const tenant: NewTenant = { + tenantType: TenantType.SHOWCASE, + id: 'test-tenant-id', + oidcIssuer: 'https://auth-server/auth/realms/test', + } + const savedTenant = await tenantRepository.create(tenant) + expect(savedTenant).toBeDefined() + + const user: NewUser = { + id: '550e8400-e29b-41d4-a716-446655440000', + userName: 'Test User', + tenants: [savedTenant.id], + } + + const savedUser = await userRepository.create(user) + + expect(savedUser).toBeDefined() + expect(savedUser.id).toEqual(user.id) + expect(savedUser.userName).toEqual(user.userName) + expect(savedUser.tenants).toHaveLength(1) + expect(savedUser.tenants).toEqual([ + expect.objectContaining({ + createdAt: expect.any(Date), + deletedAt: null, + id: 'test-tenant-id', + updatedAt: expect.any(Date), + }), + ]) + }) + + it('Should update user with tenants in database', async (): Promise => { + const tenant1: NewTenant = { + id: 'test-tenant-id-1', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + const tenant2: NewTenant = { + id: 'test-tenant-id-2', + tenantType: TenantType.SHOWCASE, + oidcIssuer: 'https://auth-server/auth/realms/test', + } + + const savedTenant1 = await tenantRepository.create(tenant1) + const savedTenant2 = await tenantRepository.create(tenant2) + expect(savedTenant1).toBeDefined() + expect(savedTenant2).toBeDefined() + + const user: NewUser = { + id: '550e8400-e29b-41d4-a716-446655440000', + userName: 'Test User', + tenants: [savedTenant1.id], + } + + const savedUser = await userRepository.create(user) + expect(savedUser).toBeDefined() + expect(savedUser.tenants).toHaveLength(1) + expect(savedUser.tenants).toEqual([ + { + createdAt: expect.any(Date), + deletedAt: null, + id: 'test-tenant-id-1', + nonceBase64: null, + oidcIssuer: 'https://auth-server/auth/realms/test', + tenantType: 'SHOWCASE', + tractionApiUrl: null, + tractionApiKey: null, + tractionTenantId: null, + tractionWalletId: null, + updatedAt: expect.any(Date), + }, + ]) + + const updatedUser = await userRepository.update(savedUser.id, { + ...savedUser, + tenants: [savedTenant2.id], + }) + + expect(updatedUser).toBeDefined() + expect(updatedUser.id).toEqual(savedUser.id) + expect(updatedUser.tenants).toHaveLength(1) + expect(updatedUser.tenants).toEqual([ + expect.objectContaining({ + id: 'test-tenant-id-2', + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + deletedAt: null, + }), + ]) + }) +}) diff --git a/apps/bc-wallet-api-server/src/database/schema/ariesProofRequest.ts b/apps/bc-wallet-api-server/src/database/schema/ariesProofRequest.ts new file mode 100644 index 000000000..895daf8e1 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/ariesProofRequest.ts @@ -0,0 +1,26 @@ +import { relations } from 'drizzle-orm' +import { pgTable, jsonb, timestamp, uuid } from 'drizzle-orm/pg-core' +import { stepActions } from './stepAction' +import { AriesRequestCredentialAttribute, AriesRequestPredicates } from '../../types' + +export const ariesProofRequests = pgTable('ariesProofRequest', { + id: uuid('id').notNull().primaryKey().defaultRandom(), + attributes: jsonb().$type>(), + predicates: jsonb().$type>(), + stepAction: uuid('step_action') + .references(() => stepActions.id, { onDelete: 'cascade' }) + .notNull() + .unique(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), +}) + +export const ariesProofRequestRelations = relations(ariesProofRequests, ({ one }) => ({ + stepAction: one(stepActions, { + fields: [ariesProofRequests.stepAction], + references: [stepActions.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/asset.ts b/apps/bc-wallet-api-server/src/database/schema/asset.ts new file mode 100644 index 000000000..8a91350e5 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/asset.ts @@ -0,0 +1,23 @@ +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { customBytea } from '../customTypes/pg' +import { relations } from 'drizzle-orm' +import { personas } from './persona' +import { showcases } from './showcase' + +export const assets = pgTable('asset', { + id: uuid('id').notNull().primaryKey().defaultRandom(), + mediaType: text('media_type').notNull(), + fileName: text('file_name'), + description: text(), + content: customBytea().notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), +}) + +export const assetRelations = relations(assets, ({ many }) => ({ + personas: many(personas), + showcases: many(showcases), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/credentialAttribute.ts b/apps/bc-wallet-api-server/src/database/schema/credentialAttribute.ts new file mode 100644 index 000000000..b209b21b2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/credentialAttribute.ts @@ -0,0 +1,31 @@ +import { index, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm' +import { CredentialAttributeTypePg } from './credentialAttributeType' +import { CredentialAttributeType } from '../../types' +import { credentialSchemas } from './credentialSchema' + +export const credentialAttributes = pgTable( + 'credentialAttribute', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + name: text().notNull(), + value: text().notNull(), + type: CredentialAttributeTypePg().notNull().$type(), + credentialSchema: uuid('credential_schema') + .references(() => credentialSchemas.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_credential_attributes').on(t.credentialSchema)], +) + +export const credentialAttributeRelations = relations(credentialAttributes, ({ one }) => ({ + credentialSchema: one(credentialSchemas, { + fields: [credentialAttributes.credentialSchema], + references: [credentialSchemas.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/credentialAttributeType.ts b/apps/bc-wallet-api-server/src/database/schema/credentialAttributeType.ts new file mode 100644 index 000000000..8c14d2c33 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/credentialAttributeType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { CredentialAttributeType } from '../../types' + +export const CredentialAttributeTypePg = pgEnum('CredentialAttributeType', Object.values(CredentialAttributeType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/credentialDefinition.ts b/apps/bc-wallet-api-server/src/database/schema/credentialDefinition.ts new file mode 100644 index 000000000..8de4a9492 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/credentialDefinition.ts @@ -0,0 +1,68 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { CredentialType, IdentifierType, Source } from '../../types' +import { assets } from './asset' +import { credentialRepresentations } from './credentialRepresentation' +import { credentialSchemas } from './credentialSchema' +import { CredentialTypePg } from './credentialType' +import { IdentifierTypePg } from './identifierType' +import { relyingPartiesToCredentialDefinitions } from './relyingPartiesToCredentialDefinitions' +import { revocationInfo } from './revocationInfo' +import { SourcePg } from './sourceType' +import { tenants } from './tenants' +import { users } from './user' + +export const credentialDefinitions = pgTable( + 'credentialDefinition', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + tenantId: text('tenant_id') + .references(() => tenants.id, { onDelete: 'cascade' }) + .notNull(), + name: text().notNull(), + version: text().notNull(), + identifierType: IdentifierTypePg('identifier_type').$type(), + identifier: text(), + credentialSchema: uuid('credential_schema') + .references(() => credentialSchemas.id) + .notNull(), + icon: uuid('icon').references(() => assets.id), + type: CredentialTypePg().notNull().$type(), + source: SourcePg().$type().notNull().default(Source.CREATED), + approvedBy: uuid('approved_by').references(() => users.id), + approvedAt: timestamp('approved_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [ + index('idx_cd_icon').on(t.icon), // Corrected index name + index('idx_cd_credentialSchema').on(t.credentialSchema), // Corrected index name + index('idx_cd_approvedBy').on(t.approvedBy), // Added index for approvedBy + ], +) + +export const credentialDefinitionRelations = relations(credentialDefinitions, ({ one, many }) => ({ + cs: one(credentialSchemas, { + fields: [credentialDefinitions.credentialSchema], + references: [credentialSchemas.id], + }), + icon: one(assets, { + fields: [credentialDefinitions.icon], + references: [assets.id], + }), + tenant: one(tenants, { + fields: [credentialDefinitions.tenantId], + references: [tenants.id], + }), + approver: one(users, { + fields: [credentialDefinitions.approvedBy], + references: [users.id], + }), + representations: many(credentialRepresentations), + revocation: one(revocationInfo), + relyingParties: many(relyingPartiesToCredentialDefinitions), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/credentialRepresentation.ts b/apps/bc-wallet-api-server/src/database/schema/credentialRepresentation.ts new file mode 100644 index 000000000..f1bc164fd --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/credentialRepresentation.ts @@ -0,0 +1,26 @@ +import { index, pgTable, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm' +import { credentialDefinitions } from './credentialDefinition' + +export const credentialRepresentations = pgTable( + 'credentialRepresentation', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + credentialDefinition: uuid('credential_definition') + .references(() => credentialDefinitions.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_credential_representations').on(t.credentialDefinition)], +) + +export const credentialRepresentationRelations = relations(credentialRepresentations, ({ one }) => ({ + credentialDefinition: one(credentialDefinitions, { + fields: [credentialRepresentations.credentialDefinition], + references: [credentialDefinitions.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/credentialSchema.ts b/apps/bc-wallet-api-server/src/database/schema/credentialSchema.ts new file mode 100644 index 000000000..1adac4eb2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/credentialSchema.ts @@ -0,0 +1,25 @@ +import { relations } from 'drizzle-orm' +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { IdentifierType, Source } from '../../types' +import { credentialAttributes } from './credentialAttribute' +import { IdentifierTypePg } from './identifierType' +import { SourcePg } from './sourceType' + +export const credentialSchemas = pgTable('credentialSchema', { + id: uuid('id').notNull().primaryKey().defaultRandom(), + identifierType: IdentifierTypePg('identifier_type').$type(), + source: SourcePg().$type().notNull().default(Source.CREATED), + identifier: text(), + name: text().notNull(), + version: text().notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), +}) + +export const credentialSchemaRelations = relations(credentialSchemas, ({ many }) => ({ + attributes: many(credentialAttributes), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/credentialType.ts b/apps/bc-wallet-api-server/src/database/schema/credentialType.ts new file mode 100644 index 000000000..c809ea947 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/credentialType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { CredentialType } from '../../types' + +export const CredentialTypePg = pgEnum('CredentialType', Object.values(CredentialType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/identifierType.ts b/apps/bc-wallet-api-server/src/database/schema/identifierType.ts new file mode 100644 index 000000000..a6f939957 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/identifierType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { IdentifierType } from '../../types' + +export const IdentifierTypePg = pgEnum('IdentifierType', Object.values(IdentifierType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/index.ts b/apps/bc-wallet-api-server/src/database/schema/index.ts new file mode 100644 index 000000000..19ec27528 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/index.ts @@ -0,0 +1,32 @@ +export * from './asset' +export * from './credentialAttribute' +export * from './credentialAttributeType' +export * from './credentialDefinition' +export * from './credentialSchema' +export * from './credentialRepresentation' +export * from './credentialType' +export * from './relyingPartiesToCredentialDefinitions' +export * from './relyingParty' +export * from './relyingPartiesToCredentialDefinitions' +export * from './identifierType' +export * from './issuer' +export * from './issuerType' +export * from './issuersToCredentialDefinitions' +export * from './issuersToCredentialSchemas' +export * from './step' +export * from './stepType' +export * from './stepAction' +export * from './scenario' +export * from './scenarioType' +export * from './revocationInfo' +export * from './persona' +export * from './scenariosToPersonas' +export * from './ariesProofRequest' +export * from './tenants' +export * from './showcase' +export * from './showcasesToCredentialDefinitions' +export * from './showcasesToPersonas' +export * from './showcaseStatus' +export * from './showcasesToScenarios' +export * from './user' +export * from './tenantsToUsers' diff --git a/apps/bc-wallet-api-server/src/database/schema/issuer.ts b/apps/bc-wallet-api-server/src/database/schema/issuer.ts new file mode 100644 index 000000000..f6636f020 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/issuer.ts @@ -0,0 +1,43 @@ +import { relations } from 'drizzle-orm' +import { pgTable, uuid, timestamp, text, index } from 'drizzle-orm/pg-core' + +import { IssuerType } from '../../types' +import { assets } from './asset' +import { issuersToCredentialDefinitions } from './issuersToCredentialDefinitions' +import { issuersToCredentialSchemas } from './issuersToCredentialSchemas' +import { IssuerTypePg } from './issuerType' +import { tenants } from './tenants' + +export const issuers = pgTable( + 'issuer', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + name: text().notNull(), + type: IssuerTypePg().notNull().$type(), + description: text().notNull(), + organization: text(), + logo: uuid().references(() => assets.id), + tenantId: text('tenant_id') + .references(() => tenants.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_issuer_logo').on(t.logo), index('idx_issuer_tenant').on(t.tenantId)], +) + +export const issuerRelations = relations(issuers, ({ one, many }) => ({ + cds: many(issuersToCredentialDefinitions), + css: many(issuersToCredentialSchemas), + logo: one(assets, { + fields: [issuers.logo], + references: [assets.id], + }), + tenant: one(tenants, { + fields: [issuers.tenantId], + references: [tenants.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/issuerType.ts b/apps/bc-wallet-api-server/src/database/schema/issuerType.ts new file mode 100644 index 000000000..445d13c76 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/issuerType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { IssuerType } from '../../types' + +export const IssuerTypePg = pgEnum('IssuerType', Object.values(IssuerType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/issuersToCredentialDefinitions.ts b/apps/bc-wallet-api-server/src/database/schema/issuersToCredentialDefinitions.ts new file mode 100644 index 000000000..f764759e3 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/issuersToCredentialDefinitions.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, primaryKey, timestamp, uuid } from 'drizzle-orm/pg-core' +import { issuers } from './issuer' +import { credentialDefinitions } from './credentialDefinition' + +export const issuersToCredentialDefinitions = pgTable( + 'issuersToCredentialDefinitions', + { + issuer: uuid() + .references(() => issuers.id, { onDelete: 'cascade' }) + .notNull(), + credentialDefinition: uuid('credential_definition') + .references(() => credentialDefinitions.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.issuer, t.credentialDefinition] }), index('idx_issuer').on(t.issuer)], +) + +export const issuersToCredentialDefinitionsRelations = relations(issuersToCredentialDefinitions, ({ one }) => ({ + cd: one(credentialDefinitions, { + fields: [issuersToCredentialDefinitions.credentialDefinition], + references: [credentialDefinitions.id], + }), + issuer: one(issuers, { + fields: [issuersToCredentialDefinitions.issuer], + references: [issuers.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/issuersToCredentialSchemas.ts b/apps/bc-wallet-api-server/src/database/schema/issuersToCredentialSchemas.ts new file mode 100644 index 000000000..168e3fe1b --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/issuersToCredentialSchemas.ts @@ -0,0 +1,28 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core' +import { issuers } from './issuer' +import { credentialSchemas } from './credentialSchema' + +export const issuersToCredentialSchemas = pgTable( + 'issuersToCredentialSchemas', + { + issuer: uuid() + .references(() => issuers.id, { onDelete: 'cascade' }) + .notNull(), + credentialSchema: uuid('credential_schema') + .references(() => credentialSchemas.id, { onDelete: 'cascade' }) + .notNull(), + }, + (t) => [primaryKey({ columns: [t.issuer, t.credentialSchema] }), index('idx_issuer_schemas').on(t.issuer)], +) + +export const issuersToCredentialSchemasRelations = relations(issuersToCredentialSchemas, ({ one }) => ({ + cs: one(credentialSchemas, { + fields: [issuersToCredentialSchemas.credentialSchema], + references: [credentialSchemas.id], + }), + issuer: one(issuers, { + fields: [issuersToCredentialSchemas.issuer], + references: [issuers.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/persona.ts b/apps/bc-wallet-api-server/src/database/schema/persona.ts new file mode 100644 index 000000000..7b8494287 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/persona.ts @@ -0,0 +1,36 @@ +import { relations } from 'drizzle-orm' +import { boolean, index, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { assets } from './asset' + +export const personas = pgTable( + 'persona', + { + id: uuid('id').defaultRandom().primaryKey(), + name: text().notNull(), + slug: text().notNull().unique(), + role: text().notNull(), + description: text().notNull(), + headshotImage: uuid('headshot_image').references(() => assets.id), + bodyImage: uuid('body_image').references(() => assets.id), + hidden: boolean().notNull().default(false), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_headshotImage').on(t.headshotImage), index('idx_bodyImage').on(t.bodyImage)], +) + +export const personaRelations = relations(personas, ({ one }) => ({ + headshotImage: one(assets, { + fields: [personas.headshotImage], + references: [assets.id], + relationName: 'headshotImage', + }), + bodyImage: one(assets, { + fields: [personas.bodyImage], + references: [assets.id], + relationName: 'bodyImage', + },), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/relyingPartiesToCredentialDefinitions.ts b/apps/bc-wallet-api-server/src/database/schema/relyingPartiesToCredentialDefinitions.ts new file mode 100644 index 000000000..83e344015 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/relyingPartiesToCredentialDefinitions.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, primaryKey, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relyingParties } from './relyingParty' +import { credentialDefinitions } from './credentialDefinition' + +export const relyingPartiesToCredentialDefinitions = pgTable( + 'relyingPartiesToCredentialDefinitions', + { + relyingParty: uuid('relying_party') + .references(() => relyingParties.id, { onDelete: 'cascade' }) + .notNull(), + credentialDefinition: uuid('credential_definition') + .references(() => credentialDefinitions.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.relyingParty, t.credentialDefinition] }), index('idx_relyingParty').on(t.relyingParty)], +) + +export const relyingPartiesToCredentialDefinitionsRelations = relations(relyingPartiesToCredentialDefinitions, ({ one }) => ({ + cd: one(credentialDefinitions, { + fields: [relyingPartiesToCredentialDefinitions.credentialDefinition], + references: [credentialDefinitions.id], + }), + rp: one(relyingParties, { + fields: [relyingPartiesToCredentialDefinitions.relyingParty], + references: [relyingParties.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/relyingParty.ts b/apps/bc-wallet-api-server/src/database/schema/relyingParty.ts new file mode 100644 index 000000000..0265590ef --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/relyingParty.ts @@ -0,0 +1,41 @@ +import { relations } from 'drizzle-orm' +import { pgTable, uuid, timestamp, text, index } from 'drizzle-orm/pg-core' + +import { RelyingPartyType } from '../../types' +import { assets } from './asset' +import { relyingPartiesToCredentialDefinitions } from './relyingPartiesToCredentialDefinitions' +import { RelyingPartyTypePg } from './relyingPartyType' +import { tenants } from './tenants' + +export const relyingParties = pgTable( + 'relyingParty', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + name: text().notNull(), + type: RelyingPartyTypePg().notNull().$type(), + description: text().notNull(), + organization: text(), + logo: uuid().references(() => assets.id), + tenantId: text('tenant_id') + .references(() => tenants.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_logo').on(t.logo), index('idx_rp_tenant').on(t.tenantId)], +) + +export const relyingPartyRelations = relations(relyingParties, ({ one, many }) => ({ + cds: many(relyingPartiesToCredentialDefinitions), + logo: one(assets, { + fields: [relyingParties.logo], + references: [assets.id], + }), + tenant: one(tenants, { + fields: [relyingParties.tenantId], + references: [tenants.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/relyingPartyType.ts b/apps/bc-wallet-api-server/src/database/schema/relyingPartyType.ts new file mode 100644 index 000000000..bcb0f4e48 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/relyingPartyType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { RelyingPartyType } from '../../types' + +export const RelyingPartyTypePg = pgEnum('RelyingPartyType', Object.values(RelyingPartyType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/revocationInfo.ts b/apps/bc-wallet-api-server/src/database/schema/revocationInfo.ts new file mode 100644 index 000000000..eee7c42b8 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/revocationInfo.ts @@ -0,0 +1,29 @@ +import { pgTable, uuid, timestamp, text, index } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm' +import { credentialDefinitions } from './credentialDefinition' + +export const revocationInfo = pgTable( + 'revocationInfo', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + title: text().notNull(), + description: text().notNull(), + credentialDefinition: uuid('credential_definition') + .references(() => credentialDefinitions.id, { onDelete: 'cascade' }) + .notNull() + .unique(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_revocation_info').on(t.credentialDefinition)], +) + +export const revocationInfoRelations = relations(revocationInfo, ({ one }) => ({ + credentialDefinition: one(credentialDefinitions, { + fields: [revocationInfo.credentialDefinition], + references: [credentialDefinitions.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/scenario.ts b/apps/bc-wallet-api-server/src/database/schema/scenario.ts new file mode 100644 index 000000000..1a73d6d13 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/scenario.ts @@ -0,0 +1,62 @@ +import { relations, sql } from 'drizzle-orm' +import { check, pgTable, text, timestamp, uuid, boolean, index } from 'drizzle-orm/pg-core' + +import { ScenarioType } from '../../types' +import { assets } from './asset' +import { issuers } from './issuer' +import { relyingParties } from './relyingParty' +import { scenariosToPersonas } from './scenariosToPersonas' +import { ScenarioTypePg } from './scenarioType' +import { steps } from './step' + +export const scenarios = pgTable( + 'scenario', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + name: text().notNull(), + slug: text().notNull().unique(), + description: text().notNull(), + scenarioType: ScenarioTypePg('scenario_type').notNull().$type(), + issuer: uuid().references(() => issuers.id, { onDelete: 'set null' }), + hidden: boolean().notNull().default(false), + relyingParty: uuid('relying_party').references(() => relyingParties.id, { onDelete: 'set null' }), + bannerImage: uuid('banner_image').references(() => assets.id), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [ + check( + 'scenario_type_check', + sql` + (scenario_type = 'PRESENTATION' AND relying_party IS NOT NULL) OR + (scenario_type = 'ISSUANCE' AND issuer IS NOT NULL) OR + (issuer IS NULL AND relying_party IS NULL) + `, + ), + index('idx_relyingParty_issuer_scenario').on(t.relyingParty), + index('idx_issuer_scenario').on(t.issuer), + index('idx_bannerImage_issuer_scenario').on(t.bannerImage), + ], +) + +export const scenarioRelations = relations(scenarios, ({ one, many }) => ({ + personas: many(scenariosToPersonas), + steps: many(steps, { + relationName: 'steps_scenario', + }), + issuer: one(issuers, { + fields: [scenarios.issuer], + references: [issuers.id], + }), + relyingParty: one(relyingParties, { + fields: [scenarios.relyingParty], + references: [relyingParties.id], + }), + bannerImage: one(assets, { + fields: [scenarios.bannerImage], + references: [assets.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/scenarioType.ts b/apps/bc-wallet-api-server/src/database/schema/scenarioType.ts new file mode 100644 index 000000000..93b570838 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/scenarioType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { ScenarioType } from '../../types' + +export const ScenarioTypePg = pgEnum('ScenarioType', Object.values(ScenarioType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/scenariosToPersonas.ts b/apps/bc-wallet-api-server/src/database/schema/scenariosToPersonas.ts new file mode 100644 index 000000000..5e27a0c26 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/scenariosToPersonas.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, primaryKey, timestamp, uuid } from 'drizzle-orm/pg-core' +import { scenarios } from './scenario' +import { personas } from './persona' + +export const scenariosToPersonas = pgTable( + 'scenariosToPersonas', + { + scenario: uuid() + .references(() => scenarios.id, { onDelete: 'cascade' }) + .notNull(), + persona: uuid() + .references(() => personas.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.scenario, t.persona] }), index('idx_scenario').on(t.scenario)], +) + +export const scenariosToPersonasRelations = relations(scenariosToPersonas, ({ one }) => ({ + scenario: one(scenarios, { + fields: [scenariosToPersonas.scenario], + references: [scenarios.id], + }), + persona: one(personas, { + fields: [scenariosToPersonas.persona], + references: [personas.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/showcase.ts b/apps/bc-wallet-api-server/src/database/schema/showcase.ts new file mode 100644 index 000000000..1262f7cc5 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/showcase.ts @@ -0,0 +1,62 @@ +import { relations } from 'drizzle-orm' +import { boolean, index, uniqueIndex, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { ShowcaseStatus } from '../../types' +import { assets } from './asset' +import { showcaseStatusPg } from './showcaseStatus' +import { showcasesToPersonas } from './showcasesToPersonas' +import { showcasesToScenarios } from './showcasesToScenarios' +import { tenants } from './tenants' +import { users } from './user' + +export const showcases = pgTable( + 'showcase', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + tenantId: text('tenant_id') + .references(() => tenants.id, { onDelete: 'cascade' }) + .notNull(), + name: text().notNull(), + slug: text().notNull(), + description: text().notNull(), + completionMessage: text(), + status: showcaseStatusPg().notNull().$type(), + hidden: boolean().notNull().default(false), + bannerImage: uuid('banner_image').references(() => assets.id), + approvedBy: uuid('approved_by').references(() => users.id), + approvedAt: timestamp('approved_at'), + createdBy: uuid('created_by').references(() => users.id), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [ + index('idx_bannerImage').on(t.bannerImage), + index('idx_tenant_id').on(t.tenantId), + index('idx_sc_approvedBy').on(t.approvedBy), + uniqueIndex('idx_tenant_slug_unique').on(t.tenantId, t.slug), + ], +) + +export const showcaseRelations = relations(showcases, ({ many, one }) => ({ + scenarios: many(showcasesToScenarios), + personas: many(showcasesToPersonas), + tenant: one(tenants, { + fields: [showcases.tenantId], + references: [tenants.id], + }), + approver: one(users, { + fields: [showcases.approvedBy], + references: [users.id], + }), + bannerImage: one(assets, { + fields: [showcases.bannerImage], + references: [assets.id], + }), + createdBy: one(users, { + fields: [showcases.createdBy], + references: [users.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/showcaseStatus.ts b/apps/bc-wallet-api-server/src/database/schema/showcaseStatus.ts new file mode 100644 index 000000000..e19366483 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/showcaseStatus.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { ShowcaseStatus } from '../../types' + +export const showcaseStatusPg = pgEnum('ShowcaseStatus', Object.values(ShowcaseStatus) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/showcasesToCredentialDefinitions.ts b/apps/bc-wallet-api-server/src/database/schema/showcasesToCredentialDefinitions.ts new file mode 100644 index 000000000..24877b785 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/showcasesToCredentialDefinitions.ts @@ -0,0 +1,32 @@ +import { relations } from 'drizzle-orm' +import { pgTable, primaryKey, timestamp, uuid, index } from 'drizzle-orm/pg-core' +import { showcases } from './showcase' +import { credentialDefinitions } from './credentialDefinition' + +export const showcasesToCredentialDefinitions = pgTable( + 'showcasesToCredentialDefinitions', + { + showcase: uuid() + .references(() => showcases.id, { onDelete: 'cascade' }) + .notNull(), + credentialDefinition: uuid('credential_definition') + .references(() => credentialDefinitions.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.showcase, t.credentialDefinition] }), index('showcases_to_credential_definitions_showcase_idx').on(t.showcase)], +) + +export const showcasesToCredentialDefinitionsRelations = relations(showcasesToCredentialDefinitions, ({ one }) => ({ + credentialDefinition: one(credentialDefinitions, { + fields: [showcasesToCredentialDefinitions.credentialDefinition], + references: [credentialDefinitions.id], + }), + showcase: one(showcases, { + fields: [showcasesToCredentialDefinitions.showcase], + references: [showcases.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/showcasesToPersonas.ts b/apps/bc-wallet-api-server/src/database/schema/showcasesToPersonas.ts new file mode 100644 index 000000000..b252d9b94 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/showcasesToPersonas.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm' +import { pgTable, primaryKey, timestamp, uuid } from 'drizzle-orm/pg-core' +import { showcases } from './showcase' +import { personas } from './persona' +import { index } from 'drizzle-orm/pg-core' + +export const showcasesToPersonas = pgTable( + 'showcasesToPersonas', + { + showcase: uuid() + .references(() => showcases.id, { onDelete: 'cascade' }) + .notNull(), + persona: uuid() + .references(() => personas.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.showcase, t.persona] }), index('showcases_to_personas_showcase_idx').on(t.showcase)], +) + +export const showcasesToPersonasRelations = relations(showcasesToPersonas, ({ one }) => ({ + persona: one(personas, { + fields: [showcasesToPersonas.persona], + references: [personas.id], + }), + showcase: one(showcases, { + fields: [showcasesToPersonas.showcase], + references: [showcases.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/showcasesToScenarios.ts b/apps/bc-wallet-api-server/src/database/schema/showcasesToScenarios.ts new file mode 100644 index 000000000..dee6beb3a --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/showcasesToScenarios.ts @@ -0,0 +1,32 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, primaryKey, timestamp, uuid } from 'drizzle-orm/pg-core' +import { showcases } from './showcase' +import { scenarios } from './scenario' + +export const showcasesToScenarios = pgTable( + 'showcasesToScenarios', + { + showcase: uuid() + .references(() => showcases.id, { onDelete: 'cascade' }) + .notNull(), + scenario: uuid() + .references(() => scenarios.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.showcase, t.scenario] }), index('showcases_to_scenarios_showcase_idx').on(t.showcase)], +) + +export const showcasesToScenariosRelations = relations(showcasesToScenarios, ({ one }) => ({ + scenario: one(scenarios, { + fields: [showcasesToScenarios.scenario], + references: [scenarios.id], + }), + showcase: one(showcases, { + fields: [showcasesToScenarios.showcase], + references: [showcases.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/sourceType.ts b/apps/bc-wallet-api-server/src/database/schema/sourceType.ts new file mode 100644 index 000000000..67208b8a5 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/sourceType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { Source } from '../../types' + +export const SourcePg = pgEnum('Source', Object.values(Source) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/step.ts b/apps/bc-wallet-api-server/src/database/schema/step.ts new file mode 100644 index 000000000..f246755c2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/step.ts @@ -0,0 +1,52 @@ +import { relations } from 'drizzle-orm' +import { index, integer, pgTable, text, timestamp, unique, uuid } from 'drizzle-orm/pg-core' +import { StepTypePg } from './stepType' +import { scenarios } from './scenario' +import { stepActions } from './stepAction' +import { assets } from './asset' +import { StepType } from '../../types' + +export const steps = pgTable( + 'step', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + title: text().notNull(), + description: text().notNull(), + order: integer().notNull(), + type: StepTypePg().notNull().$type(), + subScenario: uuid('sub_scenario').references(() => scenarios.id), + scenario: uuid() + .references(() => scenarios.id, { onDelete: 'cascade' }) + .notNull(), + asset: uuid().references(() => assets.id), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (table) => { + return { + uniqueStepOrder: unique().on(table.order, table.scenario), + scenarioIndex: index('idx_scenarios_steps').on(table.scenario), + assetIndex: index('idx_asset_steps').on(table.asset), + } + }, +) + +export const stepRelations = relations(steps, ({ one, many }) => ({ + subScenario: one(scenarios, { + fields: [steps.subScenario], + references: [scenarios.id], + }), + actions: many(stepActions), + scenario: one(scenarios, { + fields: [steps.scenario], + references: [scenarios.id], + relationName: 'steps_scenario', + }), + asset: one(assets, { + fields: [steps.asset], + references: [assets.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/stepAction.ts b/apps/bc-wallet-api-server/src/database/schema/stepAction.ts new file mode 100644 index 000000000..1e4f5c111 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/stepAction.ts @@ -0,0 +1,33 @@ +import { relations } from 'drizzle-orm' +import { index, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { steps } from './step' +import { ariesProofRequests } from './ariesProofRequest' +import { StepActionType } from '../../types' + +export const stepActions = pgTable( + 'stepAction', + { + id: uuid('id').notNull().primaryKey().defaultRandom(), + actionType: text('action_type').notNull().$type(), + title: text().notNull(), + text: text().notNull(), + step: uuid().references(() => steps.id, { onDelete: 'cascade' }), + credentialDefinitionId: text('credential_definition_id'), // TODO redesign when we get more properties + connectionId: text('connection_id'), // TODO redesign when we get more properties + goToStep: text('go_to_step'), // TODO redesign when we get more properties + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [index('idx_step_actions').on(t.step)], +) + +export const stepActionRelations = relations(stepActions, ({ one }) => ({ + step: one(steps, { + fields: [stepActions.step], + references: [steps.id], + }), + proofRequest: one(ariesProofRequests), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/stepType.ts b/apps/bc-wallet-api-server/src/database/schema/stepType.ts new file mode 100644 index 000000000..abd05ad24 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/stepType.ts @@ -0,0 +1,4 @@ +import { pgEnum } from 'drizzle-orm/pg-core' +import { StepType } from '../../types' + +export const StepTypePg = pgEnum('StepType', Object.values(StepType) as [string, ...string[]]) diff --git a/apps/bc-wallet-api-server/src/database/schema/tenants.ts b/apps/bc-wallet-api-server/src/database/schema/tenants.ts new file mode 100644 index 000000000..a13b3623a --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/tenants.ts @@ -0,0 +1,34 @@ +import { relations } from 'drizzle-orm' +import { pgEnum, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { TenantType } from '../../types' +import { issuers } from './issuer' +import { relyingParties } from './relyingParty' +import { showcases } from './showcase' +import { tenantsToUsers } from './tenantsToUsers' + +export const tenantTypePg = pgEnum('TenantType', Object.values(TenantType) as [string, ...string[]]) + +export const tenants = pgTable('tenant', { + id: text().notNull().primaryKey(), + tenantType: tenantTypePg().notNull().$type(), + oidcIssuer: text('oidc_issuer').notNull(), + tractionTenantId: uuid('traction_tenant_id'), + tractionApiUrl: text('traction_api_url'), + tractionWalletId: uuid('traction_wallet_id'), + tractionApiKey: text('traction_api_key'), + nonceBase64: text('nonce_base_64'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + deletedAt: timestamp('deleted_at'), +}) + +export const tenantRelations = relations(tenants, ({ many }) => ({ + showcase: many(showcases), + users: many(tenantsToUsers), + issuers: many(issuers), + relyingParties: many(relyingParties), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/tenantsToUsers.ts b/apps/bc-wallet-api-server/src/database/schema/tenantsToUsers.ts new file mode 100644 index 000000000..112098b10 --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/tenantsToUsers.ts @@ -0,0 +1,37 @@ +import { index, pgTable, primaryKey, text, timestamp, uuid } from 'drizzle-orm/pg-core' +import { relations } from 'drizzle-orm' +import { tenants } from './tenants' +import { users } from './user' + +export const tenantsToUsers = pgTable( + 'tenantsToUsers', + { + tenant: text() + .references(() => tenants.id, { onDelete: 'cascade' }) + .notNull(), + user: uuid() + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), + }, + (t) => [ + primaryKey({ columns: [t.tenant, t.user] }), + index('tenants_to_users_tenant_idx').on(t.tenant), + index('tenants_to_users_user_idx').on(t.user), + ], +) + +export const tenantsToUsersRelations = relations(tenantsToUsers, ({ one }) => ({ + tenant: one(tenants, { + fields: [tenantsToUsers.tenant], + references: [tenants.id], + }), + user: one(users, { + fields: [tenantsToUsers.user], + references: [users.id], + }), +})) diff --git a/apps/bc-wallet-api-server/src/database/schema/user.ts b/apps/bc-wallet-api-server/src/database/schema/user.ts new file mode 100644 index 000000000..3551c673a --- /dev/null +++ b/apps/bc-wallet-api-server/src/database/schema/user.ts @@ -0,0 +1,22 @@ +import { relations } from 'drizzle-orm' +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' + +import { showcases } from './showcase' +import { tenantsToUsers } from './tenantsToUsers' + +export const users = pgTable('user', { + id: uuid('id').notNull().primaryKey().defaultRandom(), + userName: text(), + issuer: text(), + clientId: text(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at') + .defaultNow() + .notNull() + .$onUpdate(() => new Date()), +}) + +export const userRelations = relations(users, ({ many }) => ({ + showcase: many(showcases), + tenants: many(tenantsToUsers), +})) diff --git a/apps/bc-wallet-api-server/src/index.ts b/apps/bc-wallet-api-server/src/index.ts new file mode 100644 index 000000000..4d03f64e0 --- /dev/null +++ b/apps/bc-wallet-api-server/src/index.ts @@ -0,0 +1,78 @@ +require('dotenv-flow').config() + +import 'reflect-metadata' +import * as process from 'node:process' +import { Request, Response } from 'express' +import { createExpressServer, useContainer } from 'routing-controllers' +import Container from 'typedi' + +import ApprovalController from './controllers/ApprovalController' +import AssetController from './controllers/AssetController' +import { CredentialDefinitionController } from './controllers/CredentialDefinitionController' +import { CredentialSchemaController } from './controllers/CredentialSchemaController' +import IssuanceScenarioController from './controllers/IssuanceScenarioController' +import IssuerController from './controllers/IssuerController' +import PersonaController from './controllers/PersonaController' +import PresentationScenarioController from './controllers/PresentationScenarioController' +import RelyingPartyController from './controllers/RelyingPartyController' +import ShowcaseController from './controllers/ShowcaseController' +import TenantController from './controllers/TenantController' +import { ExpressErrorHandler } from './middleware/ExpressErrorHandler' +import { RequestContextMiddleware } from './middleware/RequestContextMiddleware' +import { registerServicesByInterface } from './services/RegisterServicesByInterface' +import TenantService from './services/TenantService' +import { authorizationChecker } from './utils/auth' +import { corsOptions } from './utils/cors' + +useContainer(Container) + +async function bootstrap() { + try { + await registerServicesByInterface() + + // Create and configure Express server + const app = createExpressServer({ + controllers: [ + AssetController, + PersonaController, + CredentialSchemaController, + CredentialDefinitionController, + RelyingPartyController, + IssuerController, + IssuanceScenarioController, + PresentationScenarioController, + ShowcaseController, + TenantController, + ApprovalController, + ], + authorizationChecker, + middlewares: [RequestContextMiddleware, ExpressErrorHandler], + defaultErrorHandler: false, + cors: corsOptions, + }) + + const tenantService = Container.get(TenantService) + void (await tenantService.createRootTenant()) + + // Start the server + const port = Number(process.env.PORT) + + // Health check endpoint + app.get('/health', (_req: Request, res: Response) => { + res.status(200).json({ + status: 'healthy', + timestamp: new Date().toISOString(), + }) + }) + + app.listen(port, (): void => { + console.log(`Server is running on port ${port}`) + }) + } catch (error) { + console.error('Failed to start application:', error) + process.exit(1) + } +} + +// Start the application +void bootstrap() diff --git a/apps/bc-wallet-api-server/src/middleware/ExpressErrorHandler.ts b/apps/bc-wallet-api-server/src/middleware/ExpressErrorHandler.ts new file mode 100644 index 000000000..5937c8717 --- /dev/null +++ b/apps/bc-wallet-api-server/src/middleware/ExpressErrorHandler.ts @@ -0,0 +1,29 @@ +import { Response } from 'express' +import { + ExpressErrorMiddlewareInterface, + HttpError, + Middleware, + NotFoundError, + UnauthorizedError, +} from 'routing-controllers' +import { Service } from 'typedi' + +@Service() +@Middleware({ type: 'after' }) +export class ExpressErrorHandler implements ExpressErrorMiddlewareInterface { + error(error: HttpError | UnauthorizedError, request: Request, response: Response): void { + if (error instanceof NotFoundError) { + response.status(404).json({ + message: error.message ?? 'Not Found', + }) + } else if (error instanceof UnauthorizedError) { + response.status(401).json({ + message: error.message ?? 'Unauthorized', + }) + } else { + response.status(500).json({ + message: error.message ?? 'Internal server error', + }) + } + } +} diff --git a/apps/bc-wallet-api-server/src/middleware/RequestContextMiddleware.ts b/apps/bc-wallet-api-server/src/middleware/RequestContextMiddleware.ts new file mode 100644 index 000000000..a4299e8ab --- /dev/null +++ b/apps/bc-wallet-api-server/src/middleware/RequestContextMiddleware.ts @@ -0,0 +1,66 @@ +import { NextFunction, Request, Response } from 'express' +import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers' +import { Inject, Service } from 'typedi' + +import { Token } from '../types/auth/token' +import { ISessionServiceUpdater } from '../types/services/session' + +@Service() +@Middleware({ type: 'before' }) +export class RequestContextMiddleware implements ExpressMiddlewareInterface { + public constructor(@Inject('ISessionService') private readonly sessionService: ISessionServiceUpdater) {} + + public use(request: Request, _: Response, next: NextFunction): void { + const authHeader = request.headers['authorization'] + const apiBaseUrl = this.buildApiBaseUrl(request) + const bearerToken = authHeader && authHeader.startsWith('Bearer ') ? authHeader.substring(7) : undefined + const token = bearerToken ? new Token(bearerToken) : undefined + this.sessionService.setRequestDetails(apiBaseUrl, this.getUrlTokenId(request), token) + + next() + } + + private getUrlTokenId(request: Request): string | null { + const path = request.path ?? '' + const segments = path.split('/').filter(Boolean) + if (segments.length < 2) { + return null + } + // /tenants are the only endpoints without tenant in its path + if (segments.length > 0 && segments[0] === 'tenants') { + return null + } + return segments[0] + } + + private buildApiBaseUrl(request: Request): string { + const protocol = request.headers['x-forwarded-proto']?.toString() ?? request.protocol + let host = request.headers['x-forwarded-host']?.toString() ?? request.get('host') + const forwardedPort = request.headers['x-forwarded-port']?.toString() + const forwardedPrefix = request.headers['x-forwarded-prefix']?.toString() ?? '' + + if (!host) { + throw new Error('Host header is missing') + } + + if (forwardedPort) { + let baseHost: string + + // Strip any existing port from the host value + // Handle IPv6 address format like [::1]:8080 + if (host.startsWith('[')) { + const closingBracketIndex = host.indexOf(']') + baseHost = closingBracketIndex !== -1 ? host.substring(0, closingBracketIndex + 1) : host + } else { + // Handle IPv4 or hostname format like localhost:8080 by taking the part before the first colon + baseHost = host.split(':')[0] + } + const isDefaultForProtocol = + (protocol === 'https' && forwardedPort === '443') || (protocol === 'http' && forwardedPort === '80') + + host = isDefaultForProtocol ? baseHost : `${baseHost}:${forwardedPort}` + } + + return `${protocol}://${host}${forwardedPrefix}` + } +} diff --git a/apps/bc-wallet-api-server/src/services/AbstractAdapterClientService.ts b/apps/bc-wallet-api-server/src/services/AbstractAdapterClientService.ts new file mode 100644 index 000000000..d39ad5f49 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/AbstractAdapterClientService.ts @@ -0,0 +1,27 @@ +import { SendOptions } from 'bc-wallet-adapter-client-api' + +import type { ISessionService } from '../types/services/session' +import TenantService from './TenantService' + +export abstract class AbstractAdapterClientService { + protected constructor( + protected readonly sessionService: ISessionService, + private readonly tenantService: TenantService, + ) {} + + protected async buildSendOptions(): Promise { + const tenant = this.sessionService.getCurrentTenant() + if (!tenant) { + throw new Error('Current tenant not set') + } + const fullTenant = await this.tenantService.getTenant(tenant.id, true) // User's tenant record is redacted and does not contain Traction secrets + + return { + authHeader: this.sessionService.getBearerToken()?.getRawToken(), + showcaseApiUrlBase: `${this.sessionService.getApiBaseUrl()}/${fullTenant.id}`, + ...(fullTenant.tractionApiUrl && { tractionApiUrlBase: fullTenant.tractionApiUrl }), + ...(fullTenant.tractionTenantId && { tractionTenantId: fullTenant.tractionTenantId }), + ...(fullTenant.tractionWalletId && { tractionWalletId: fullTenant.tractionWalletId }), + } + } +} diff --git a/apps/bc-wallet-api-server/src/services/AssetService.ts b/apps/bc-wallet-api-server/src/services/AssetService.ts new file mode 100644 index 000000000..e9f1f85c7 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/AssetService.ts @@ -0,0 +1,30 @@ +import { Service } from 'typedi' +import AssetRepository from '../database/repositories/AssetRepository' +import { Asset, NewAsset } from '../types' + +@Service() +class AssetService { + constructor(private readonly assetRepository: AssetRepository) {} + + public getAssets = async (): Promise => { + return this.assetRepository.findAll() + } + + public getAsset = async (id: string): Promise => { + return this.assetRepository.findById(id) + } + + public createAsset = async (asset: NewAsset): Promise => { + return this.assetRepository.create(asset) + } + + public updateAsset = async (id: string, asset: NewAsset): Promise => { + return this.assetRepository.update(id, asset) + } + + public deleteAsset = async (id: string): Promise => { + return this.assetRepository.delete(id) + } +} + +export default AssetService diff --git a/apps/bc-wallet-api-server/src/services/CredentialDefinitionService.ts b/apps/bc-wallet-api-server/src/services/CredentialDefinitionService.ts new file mode 100644 index 000000000..c4dc5ed96 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/CredentialDefinitionService.ts @@ -0,0 +1,185 @@ +import { IAdapterClientApi } from 'bc-wallet-adapter-client-api' +import { type CredentialDefinitionImportRequest } from 'bc-wallet-openapi' +import { ForbiddenError, InternalServerError } from 'routing-controllers' +import { Inject, Service } from 'typedi' +import { validate as uuidValidate } from 'uuid' + +import CredentialDefinitionRepository from '../database/repositories/CredentialDefinitionRepository' +import { CredentialDefinition, NewCredentialDefinition } from '../types' +import { ISessionService } from '../types/services/session' +import { AbstractAdapterClientService } from './AbstractAdapterClientService' +import TenantService from './TenantService' + +/** + * Service for managing credential definitions. + * Provides methods for CRUD operations, approval workflow, and integration with adapter client API. + * Extends AbstractAdapterClientService to utilize session-related functionality. + */ +@Service() +class CredentialDefinitionService extends AbstractAdapterClientService { + public constructor( + @Inject('ISessionService') sessionService: ISessionService, + @Inject('IAdapterClientApi') private readonly adapterClientApi: IAdapterClientApi, + private readonly credentialDefinitionRepository: CredentialDefinitionRepository, + tenantService: TenantService, + ) { + super(sessionService, tenantService) + } + + /** + * Retrieves all credential definitions. + * @returns Promise resolving to an array of CredentialDefinition objects + */ + public getCredentialDefinitions = async (): Promise => { + this.sessionService.getCurrentTenant() + return this.credentialDefinitionRepository.findAll(this.getTenantId()) + } + + /** + * Retrieves a specific credential definition by ID. + * @param id The ID of the credential definition to retrieve + * @returns Promise resolving to the requested CredentialDefinition + */ + public getCredentialDefinition = async (id: string): Promise => { + if (!uuidValidate(id) && id.split(':').length >= 3) { + // support for lookup by indy identifier + return this.credentialDefinitionRepository.findByIdentifier(id) + } + + return this.credentialDefinitionRepository.findById(id) // TODO is demo-web using this endpoint or can we filter on tenantId? + } + + /** + * Retrieves all unapproved credential definitions. + * @returns Promise resolving to an array of unapproved CredentialDefinition objects + */ + public getUnapproved = async (): Promise => { + return this.credentialDefinitionRepository.findUnapproved(this.getTenantId()) + } + + /** + * Creates a new credential definition. + * @param credentialDefinition The new credential definition data + * @returns Promise resolving to the created CredentialDefinition + */ + public createCredentialDefinition = async ( + credentialDefinition: NewCredentialDefinition, + ): Promise => { + const tenantId = this.getTenantId() + if (!credentialDefinition.tenantId) { + credentialDefinition.tenantId = tenantId + } else if (credentialDefinition.tenantId !== tenantId) { + throw new ForbiddenError( + `The credential definition is being created for tenant ${credentialDefinition.tenantId}, but the signed in tenant is ${tenantId}.`, + ) + } + + return this.credentialDefinitionRepository.create(credentialDefinition) + } + + /** + * Updates an existing credential definition. + * @param id The ID of the credential definition to update + * @param credentialDefinition The updated credential definition data + * @returns Promise resolving to the updated CredentialDefinition + */ + public updateCredentialDefinition = async ( + id: string, + credentialDefinition: NewCredentialDefinition, + ): Promise => { + const tenantId = this.getTenantId() + if (!credentialDefinition.tenantId) { + credentialDefinition.tenantId = tenantId + } else if (credentialDefinition.tenantId !== tenantId) { + throw new ForbiddenError( + `The credential definition is being updated for tenant ${credentialDefinition.tenantId}, but the signed in tenant is ${tenantId}.`, + ) + } + + return this.credentialDefinitionRepository.update(id, credentialDefinition) + } + + /** + * Deletes a credential definition. + * @param id The ID of the credential definition to delete + * @returns Promise resolving when deletion is complete + */ + public deleteCredentialDefinition = async (id: string): Promise => { + const existingDef = await this.credentialDefinitionRepository.findById(id) + if (existingDef) { + const tenantId = this.getTenantId() + if (existingDef.tenantId !== tenantId) + return Promise.reject( + new ForbiddenError( + `The credential definition is being deleted for tenant ${existingDef.tenantId}, but the signed in tenant is ${tenantId}.`, + ), + ) + } else { + return + } + + return this.credentialDefinitionRepository.delete(id) + } + + /** + * Imports a credential definition into the system and adapter. + * @param importRequest The credential definition to import + * @returns Promise resolving when import is complete + * @throws Error if identifier type or identifier is missing + */ + public importCredentialDefinition = async (importRequest: CredentialDefinitionImportRequest): Promise => { + if (!importRequest.identifierType || !importRequest.identifier) { + return Promise.reject(Error('Identifier type and identifier are required for credential definition import.')) + } + + await this.adapterClientApi.importCredentialDefinition(importRequest, await this.buildSendOptions()) + } + + /** + * Approves a credential definition. + * @param id The ID of the credential definition to approve + * @returns Promise resolving to the updated CredentialDefinition + * @throws NotFoundError if the definition doesn't exist + * @throws Error if the current user cannot be determined + */ + public approveCredentialDefinition = async (id: string): Promise => { + const currentUser = await this.sessionService.getCurrentUser() + if (!currentUser) { + return Promise.reject(new Error('Could not determine the approving user.')) + } + + const existingDef = await this.credentialDefinitionRepository.findById(id) + if (existingDef) { + const tenantId = this.getTenantId() + if (existingDef.tenantId !== tenantId) + return Promise.reject( + new ForbiddenError( + `The credential definition is being approved for tenant ${existingDef.tenantId}, but the signed in tenant is ${tenantId}.`, + ), + ) + } + + return this.credentialDefinitionRepository.approve(id, currentUser.id) + } + + /** + * Checks if a credential definition is approved. + * @param id The ID of the credential definition to check + * @returns Promise resolving to boolean indicating approval status + */ + public isApproved = async (id: string): Promise => { + const credentialDef = await this.credentialDefinitionRepository.findById(id) + return credentialDef.approvedBy !== undefined && credentialDef.approvedBy !== null + } + + private getTenantId() { + // TODO Deduplicate with showcase service + const urlTenantId = this.sessionService.getUrlTenantId() + if (!urlTenantId) { + throw new InternalServerError('Tenant details are missing') + } + return urlTenantId + } +} + +export default CredentialDefinitionService diff --git a/apps/bc-wallet-api-server/src/services/CredentialSchemaService.ts b/apps/bc-wallet-api-server/src/services/CredentialSchemaService.ts new file mode 100644 index 000000000..9f631223b --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/CredentialSchemaService.ts @@ -0,0 +1,101 @@ +import { IAdapterClientApi } from 'bc-wallet-adapter-client-api' +import { CredentialDefinitionImportRequest } from 'bc-wallet-openapi' +import { Inject, Service } from 'typedi' +import { validate as uuidValidate } from 'uuid' + +import CredentialSchemaRepository from '../database/repositories/CredentialSchemaRepository' +import { CredentialSchema, NewCredentialSchema } from '../types' +import type { ISessionService } from '../types/services/session' +import { AbstractAdapterClientService } from './AbstractAdapterClientService' +import TenantService from './TenantService' + +/** + * Service for managing credential schemas. + * Handles CRUD operations and integration with adapter client API. + * Extends AbstractAdapterClientService for session functionality. + */ +@Service() +class CredentialSchemaService extends AbstractAdapterClientService { + /** + * Constructor for CredentialSchemaService. + * @param sessionService Service for managing user sessions + * @param adapterClientApi Client API for interacting with the wallet adapter + * @param credentialSchemaRepository Repository for credential schema data access + */ + public constructor( + @Inject('ISessionService') sessionService: ISessionService, + @Inject('IAdapterClientApi') private readonly adapterClientApi: IAdapterClientApi, + private readonly credentialSchemaRepository: CredentialSchemaRepository, + tenantService: TenantService, + ) { + super(sessionService, tenantService) + } + + /** + * Retrieves all credential schemas. + * @returns Promise resolving to an array of CredentialSchema objects + */ + public getCredentialSchemas = async (): Promise => { + return this.credentialSchemaRepository.findAll() + } + + /** + * Retrieves a specific credential schema by ID. + * @param id The ID of the credential schema to retrieve + * @returns Promise resolving to the requested CredentialSchema + */ + public getCredentialSchema = async (id: string): Promise => { + if (!uuidValidate(id) && id.split(':').length >= 3) { + // support for lookup by indy identifier + return this.credentialSchemaRepository.findByIdentifier(id) + } + return this.credentialSchemaRepository.findById(id) + } + + /** + * Creates a new credential schema. + * @param credentialSchema The new credential schema data + * @returns Promise resolving to the created CredentialSchema + */ + public createCredentialSchema = async (credentialSchema: NewCredentialSchema): Promise => { + return this.credentialSchemaRepository.create(credentialSchema) + } + + /** + * Updates an existing credential schema. + * @param id The ID of the credential schema to update + * @param credentialSchema The updated credential schema data + * @returns Promise resolving to the updated CredentialSchema + */ + public updateCredentialSchema = async ( + id: string, + credentialSchema: NewCredentialSchema, + ): Promise => { + return this.credentialSchemaRepository.update(id, credentialSchema) + } + + /** + * Deletes a credential schema. + * @param id The ID of the credential schema to delete + * @returns Promise resolving when deletion is complete + */ + public deleteCredentialSchema = async (id: string): Promise => { + return this.credentialSchemaRepository.delete(id) + } + + /** + * Imports a credential schema into the system and adapter. + * @param importRequest The credential schema to import + * @returns Promise resolving when import is complete + * @throws Error if identifier type or identifier is missing + */ + public importCredentialSchema = async (importRequest: CredentialDefinitionImportRequest): Promise => { + if (!importRequest.identifierType || !importRequest.identifier) { + return Promise.reject(Error('Identifier type and identifier are required for credential schema import.')) + } + + await this.adapterClientApi.importCredentialSchema(importRequest, await this.buildSendOptions()) + } +} + +export default CredentialSchemaService diff --git a/apps/bc-wallet-api-server/src/services/DatabaseService.ts b/apps/bc-wallet-api-server/src/services/DatabaseService.ts new file mode 100644 index 000000000..75b097037 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/DatabaseService.ts @@ -0,0 +1,35 @@ +import type { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { drizzle } from 'drizzle-orm/node-postgres' +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { Pool } from 'pg' +import { Service } from 'typedi' + +import * as schema from '../database/schema' + +const path = require('path') + +@Service() +export class DatabaseService { + private db?: NodePgDatabase + + private getDbUrl(): string { + return ( + process.env.DB_URL ?? + `postgresql://${process.env.SHOWCASE_DB_USERNAME}${process.env.SHOWCASE_DB_PASSWORD && `:${process.env.SHOWCASE_DB_PASSWORD}`}${process.env.SHOWCASE_DB_HOST && `@${process.env.SHOWCASE_DB_HOST}`}${process.env.SHOWCASE_DB_PORT && `:${process.env.SHOWCASE_DB_PORT}`}/${process.env.SHOWCASE_DB_NAME}` + ) + } + + public async getConnection(): Promise> { + if (!this.db) { + const pool = new Pool({ connectionString: this.getDbUrl() }) + this.db = drizzle(pool, { schema }) + + const migrationsFolder = path.resolve(__dirname, '../database/migrations') + await migrate(this.db, { migrationsFolder }) + } + + return this.db + } +} + +export default DatabaseService diff --git a/apps/bc-wallet-api-server/src/services/DuplicationShowcaseService.ts b/apps/bc-wallet-api-server/src/services/DuplicationShowcaseService.ts new file mode 100644 index 000000000..4d6069616 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/DuplicationShowcaseService.ts @@ -0,0 +1,190 @@ +import { Service } from 'typedi' + +import PersonaRepository from '../database/repositories/PersonaRepository' +import ScenarioRepository from '../database/repositories/ScenarioRepository' +import ShowcaseRepository from '../database/repositories/ShowcaseRepository' +import { + IssuanceScenario, + NewShowcase, + NewStep, + NewStepActionTypes, + PresentationScenario, + ScenarioType, + Showcase, + ShowcaseStatus, +} from '../types' +// import { ISessionService } from '../types/services/session' + +/** + * Extracts just the ID from an object or returns the ID if it's already a string + * @param obj Object or string ID + * @returns String ID + */ +const extractId = (obj: any): string => { + if (typeof obj === 'string') return obj + if (obj && typeof obj === 'object' && 'id' in obj) return obj.id + return '' +} + +/** + * Removes specified fields from an object and returns a new object + * @param obj The source object + * @param fieldsToRemove Array of field names to remove + * @returns A new object without the specified fields + */ +const removeFields = >(obj: T, fieldsToRemove: string[]): Partial => { + const newObj: Partial = {} + + for (const key in obj) { + if (!fieldsToRemove.includes(key)) { + newObj[key] = obj[key] + } + } + + return newObj +} + +/** + * Extracts the ID from an object or returns null if the object is null + * @param obj The object to extract the ID from + * @returns The ID of the object or null if the object is null + */ +const extractIdOrNull = (obj: any) => (obj ? extractId(obj) : null) + +@Service() +@Service() +class DuplicationShowcaseService { + public constructor( + private readonly showcaseRepository: ShowcaseRepository, + private readonly personaRepository: PersonaRepository, + private readonly scenarioRepository: ScenarioRepository, + // @Inject('ISessionService') private readonly sessionService: ISessionService, + ) {} + + public duplicateShowcase = async (id: string): Promise => { + const showcase = await this.showcaseRepository.findById(id) + const newPersonaIds: string[] = [] + const newScenarioIds: string[] = [] + const personaIdMap: Record = {} + + const currentUserId = showcase.createdBy?.id + const tenantId = showcase.tenantId + // const tenant = await this.sessionService.getCurrentTenant() + + // if (!tenant) { + // throw new Error('Tenant not found') + // } + + // const currentUserId = await this.sessionService.getCurrentUser() + // if (!currentUserId) { + // throw new Error('Could not determine current logged in user.') + // } + + if (showcase.personas && showcase.personas.length > 0) { + for (const personaObj of showcase.personas) { + const persona = personaObj + + const personaRequest = { + name: persona.name, + role: persona.role, + description: persona.description, + headshotImage: extractId(persona.headshotImage), + bodyImage: extractId(persona.bodyImage), + hidden: persona.hidden, + } + + const newPersona = await this.personaRepository.create(personaRequest) + personaIdMap[persona.id] = newPersona.id + newPersonaIds.push(newPersona.id) + } + } + + if (showcase.scenarios && showcase.scenarios.length > 0) { + for (const scenarioObj of showcase.scenarios) { + const cleanedSteps = scenarioObj.steps.map((step) => { + const cleanedStep = removeFields(step, ['id', 'createdAt', 'updatedAt']) as NewStep + cleanedStep.asset = step.asset ? extractIdOrNull(step.asset) : null + + if (cleanedStep.actions && Array.isArray(cleanedStep.actions)) { + cleanedStep.actions = cleanedStep.actions.map((action) => { + const cleanedAction = removeFields(action, ['id', 'createdAt', 'updatedAt']) + + if ('proofRequest' in cleanedAction) { + cleanedAction.proofRequest = { + attributes: { + ...cleanedAction.proofRequest?.attributes, + }, + predicates: { + ...cleanedAction.proofRequest?.predicates, + }, + } + } + + return cleanedAction as NewStepActionTypes + }) + } + + return cleanedStep + }) + + const mappedPersonaIds = scenarioObj.personas.map((p) => personaIdMap[extractId(p)] || extractId(p)) + + const scenarioType = scenarioObj.scenarioType + + const baseScenarioData = { + name: scenarioObj.name, + description: scenarioObj.description || '', + scenarioType, + steps: cleanedSteps, + hidden: scenarioObj.hidden, + personas: mappedPersonaIds, + } + + let newScenario: IssuanceScenario | PresentationScenario | null = null + + if (scenarioType === ScenarioType.ISSUANCE) { + if ('issuer' in scenarioObj && scenarioObj.issuer && 'id' in scenarioObj.issuer) { + const payload = { + ...baseScenarioData, + issuer: scenarioObj.issuer.id, + } + newScenario = await this.scenarioRepository.create(payload) + } else { + throw new Error('Issuer not found in scenario') + } + } else if (scenarioType === ScenarioType.PRESENTATION) { + if ('relyingParty' in scenarioObj && scenarioObj.relyingParty && 'id' in scenarioObj.relyingParty) { + const payload = { + ...baseScenarioData, + relyingParty: scenarioObj.relyingParty.id, + } + newScenario = await this.scenarioRepository.create(payload) + } else { + throw new Error('Relying party not found in scenario') + } + } + + if (newScenario) { + newScenarioIds.push(newScenario.id) + } + } + } + + const newShowcase: NewShowcase = { + name: `${showcase.name} (Copy)`, + description: showcase.description, + status: ShowcaseStatus.ACTIVE, + createdBy: currentUserId, + bannerImage: showcase.bannerImage ? extractId(showcase.bannerImage) : undefined, + scenarios: newScenarioIds, + personas: newPersonaIds, + tenantId: tenantId, + hidden: showcase.hidden, + completionMessage: showcase.completionMessage, + } + + return this.showcaseRepository.create(newShowcase) + } +} + +export default DuplicationShowcaseService diff --git a/apps/bc-wallet-api-server/src/services/IssuerService.ts b/apps/bc-wallet-api-server/src/services/IssuerService.ts new file mode 100644 index 000000000..f7a321d24 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/IssuerService.ts @@ -0,0 +1,52 @@ +import { IAdapterClientApi } from 'bc-wallet-adapter-client-api' +import { Inject, Service } from 'typedi' + +import IssuerRepository from '../database/repositories/IssuerRepository' +import type { Issuer, NewIssuer } from '../types' +import type { ISessionService } from '../types/services/session' +import { issuerDTOFrom } from '../utils/mappers' +import { AbstractAdapterClientService } from './AbstractAdapterClientService' +import TenantService from './TenantService' + +@Service() +class IssuerService extends AbstractAdapterClientService { + public constructor( + private readonly issuerRepository: IssuerRepository, + @Inject('ISessionService') sessionService: ISessionService, + @Inject('IAdapterClientApi') private readonly adapterClientApi: IAdapterClientApi, + tenantService: TenantService, + ) { + super(sessionService, tenantService) + } + + public getIssuers = async (): Promise => { + return this.issuerRepository.findAll() + } + + public getIssuer = async (id: string): Promise => { + return this.issuerRepository.findById(id) + } + + public createIssuer = async (newIssuer: NewIssuer): Promise => { + const createdIssuer = await this.issuerRepository.create(newIssuer) + void (await this.adapterClientApi.publishIssuer(issuerDTOFrom(createdIssuer), await this.buildSendOptions())) + return createdIssuer + } + + public updateIssuer = async (id: string, newIssuer: NewIssuer): Promise => { + const updatedIssuer = await this.issuerRepository.update(id, newIssuer) + void (await this.adapterClientApi.publishIssuer(issuerDTOFrom(updatedIssuer), await this.buildSendOptions())) + return updatedIssuer + } + + public publishIssuer = async (id: string): Promise => { + const issuer = await this.issuerRepository.findById(id) + void (await this.adapterClientApi.publishIssuer(issuerDTOFrom(issuer), await this.buildSendOptions())) + } + + public deleteIssuer = async (id: string): Promise => { + return this.issuerRepository.delete(id) + } +} + +export default IssuerService diff --git a/apps/bc-wallet-api-server/src/services/OidcSessionService.ts b/apps/bc-wallet-api-server/src/services/OidcSessionService.ts new file mode 100644 index 000000000..aff2c16b9 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/OidcSessionService.ts @@ -0,0 +1,151 @@ +import { AsyncLocalStorage } from 'async_hooks' +import { LRUCache } from 'lru-cache' +import { Service } from 'typedi' + +import type { Tenant, User } from '../types' +import { Claims } from '../types/auth/claims' +import type { OidcSession } from '../types/auth/session' +import { Token } from '../types/auth/token' +import type { ISessionService, ISessionServiceUpdater } from '../types/services/session' +import UserService from './UserService' + +const sessionCache = new LRUCache({ + max: 65535, + ttl: 5 * 60 * 1000, // We toss sessions after 5 minutes by default. This will be overridden by validated tokens. + allowStale: false, +}) + +// per-request store for current token hash +export const tokenHashStore = new AsyncLocalStorage() +export const tenantUrlStore = new AsyncLocalStorage() + +@Service() +export class OidcSessionService implements ISessionService, ISessionServiceUpdater { + public constructor(private readonly userService: UserService) {} + + public getCurrentUser(): User | null { + return this.getSession().user + } + + public getCurrentTenant(): Tenant | null { + return this.getSession().tenant + } + + public getUrlTenantId(): string | null { + return this.getSession().urlTenantId ?? (tenantUrlStore.getStore() as string) + } + + public getBearerToken(): Token | undefined { + return this.getSession().bearerToken + } + + public getApiBaseUrl(): string | undefined { + return this.getSession().apiBaseUrl + } + + public getActiveClaims(): Claims | undefined { + return this.getSession().activeClaims + } + + public setRequestDetails(apiBaseUrl: string, urlTenantId: string | null, token?: Token): void { + if (!token) { + // We won't have a session if we don't have a bearer token, use a request scoped var instead + if (urlTenantId) { + tenantUrlStore.enterWith(urlTenantId) + } + return + } + const hash = token.getSignatureHash() + tokenHashStore.enterWith(hash) + let session = sessionCache.get(hash) + if (!session) { + session = { user: null, urlTenantId: urlTenantId, tenant: null } + // Add to cache with default TTL. If token is validated later, TTL will be updated. + sessionCache.set(hash, session) + } + session.apiBaseUrl = apiBaseUrl + session.urlTenantId = urlTenantId + session.bearerToken = token // Store the raw token, may not be validated yet + } + + public setActiveClaims(claims: Claims): void { + const hash = this.getCurrentHash() + if (!hash) { + console.warn('Attempted to set active claims without a token in context.') + return + } + const session = this.getSession() + session.activeClaims = claims + } + + public async setCurrentUser(userName: string): Promise { + const session = this.getSession() + if (!session.user) { + if (!session.tenant) { + return Promise.reject(new Error('Tenant is not set')) + } + try { + session.user = await this.userService.getUserByNameAndTenantId(userName, session.tenant.id) + } catch { + session.user = await this.userService.createUser({ userName }) + } + } + } + + public setCurrentTenant(value: Tenant): void { + const session = this.getSession() + session.tenant = value + } + + public clear(): void { + const hash = this.getCurrentHash() + sessionCache.delete(hash) + tokenHashStore.disable() // Clears the store for the current async execution path + tenantUrlStore.disable() + } + + public getCachedSessionByTokenHash(tokenHash: string): OidcSession | undefined { + return sessionCache.get(tokenHash) + } + + public cacheValidatedToken(tokenHash: string, token: Token, claims: Claims, ttlMs: number): void { + if (ttlMs <= 0) { + // If TTL is not positive, don't cache it + let existingSession = sessionCache.get(tokenHash) + if (existingSession) { + existingSession.bearerToken = token + existingSession.activeClaims = claims + } + return + } + + let session = sessionCache.get(tokenHash) + if (!session) { + session = { user: null, urlTenantId: null, tenant: null } + } + session.bearerToken = token + session.activeClaims = claims + sessionCache.set(tokenHash, session, { ttl: ttlMs }) + } + + private getCurrentHash(): string { + const hash = tokenHashStore.getStore() + if (!hash) { + throw new Error('No token signature hash in context') + } + return hash + } + + private getSession(): OidcSession { + const hash = tokenHashStore.getStore() + if (!hash) { + return { user: null, urlTenantId: null, tenant: null } + } + let session = sessionCache.get(hash) + if (!session) { + session = { user: null, urlTenantId: null, tenant: null } + sessionCache.set(hash, session) + } + return session + } +} diff --git a/apps/bc-wallet-api-server/src/services/PersonaService.ts b/apps/bc-wallet-api-server/src/services/PersonaService.ts new file mode 100644 index 000000000..51e6c4fdd --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/PersonaService.ts @@ -0,0 +1,34 @@ +import { Service } from 'typedi' +import PersonaRepository from '../database/repositories/PersonaRepository' +import { NewPersona, Persona } from '../types' + +@Service() +class PersonaService { + constructor(private readonly personaRepository: PersonaRepository) {} + + public getAll = async (): Promise => { + return this.personaRepository.findAll() + } + + public get = async (id: string): Promise => { + return this.personaRepository.findById(id) + } + + public create = async (persona: NewPersona): Promise => { + return this.personaRepository.create(persona) + } + + public update = async (id: string, persona: NewPersona): Promise => { + return this.personaRepository.update(id, persona) + } + + public delete = async (id: string): Promise => { + return this.personaRepository.delete(id) + } + + public getIdBySlug = async (slug: string): Promise => { + return this.personaRepository.findIdBySlug(slug) + } +} + +export default PersonaService diff --git a/apps/bc-wallet-api-server/src/services/RegisterServicesByInterface.ts b/apps/bc-wallet-api-server/src/services/RegisterServicesByInterface.ts new file mode 100644 index 000000000..f0c528f85 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/RegisterServicesByInterface.ts @@ -0,0 +1,11 @@ +import { AdapterClientApi } from 'bc-wallet-adapter-client-api' +import Container from 'typedi' + +import { OidcSessionService } from './OidcSessionService' + +export async function registerServicesByInterface() { + const oidcSessionService = Container.get(OidcSessionService) + Container.set('ISessionService', oidcSessionService) + const adapterClientApi = Container.get(AdapterClientApi) + Container.set('IAdapterClientApi', adapterClientApi) +} diff --git a/apps/bc-wallet-api-server/src/services/RelyingPartyService.ts b/apps/bc-wallet-api-server/src/services/RelyingPartyService.ts new file mode 100644 index 000000000..b83059bf7 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/RelyingPartyService.ts @@ -0,0 +1,30 @@ +import { Service } from 'typedi' +import RelyingPartyRepository from '../database/repositories/RelyingPartyRepository' +import { RelyingParty, NewRelyingParty } from '../types' + +@Service() +class RelyingPartyService { + constructor(private readonly relyingPartyRepository: RelyingPartyRepository) {} + + public getRelyingParties = async (): Promise => { + return this.relyingPartyRepository.findAll() + } + + public getRelyingParty = async (id: string): Promise => { + return this.relyingPartyRepository.findById(id) + } + + public createRelyingParty = async (relyingParty: NewRelyingParty): Promise => { + return this.relyingPartyRepository.create(relyingParty) + } + + public updateRelyingParty = async (id: string, relyingParty: NewRelyingParty): Promise => { + return this.relyingPartyRepository.update(id, relyingParty) + } + + public deleteRelyingParty = async (id: string): Promise => { + return this.relyingPartyRepository.delete(id) + } +} + +export default RelyingPartyService diff --git a/apps/bc-wallet-api-server/src/services/ScenarioService.ts b/apps/bc-wallet-api-server/src/services/ScenarioService.ts new file mode 100644 index 000000000..799048a0d --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/ScenarioService.ts @@ -0,0 +1,79 @@ +import { Service } from 'typedi' +import ScenarioRepository from '../database/repositories/ScenarioRepository' +import { NewScenario, NewStep, NewStepActionTypes, Scenario, ScenarioFindAllArgs, Step, StepActionTypes } from '../types' + +@Service() +class ScenarioService { + constructor(private readonly scenarioRepository: ScenarioRepository) {} + + public getScenarios = async (args: ScenarioFindAllArgs): Promise => { + return this.scenarioRepository.findAll(args) + } + + public getScenario = async (scenarioId: string): Promise => { + return this.scenarioRepository.findById(scenarioId) + } + + public createScenario = async (scenario: NewScenario): Promise => { + return this.scenarioRepository.create(scenario) + } + + public updateScenario = async (scenarioId: string, scenario: NewScenario): Promise => { + return this.scenarioRepository.update(scenarioId, scenario) + } + + public deleteScenario = async (scenarioId: string): Promise => { + return this.scenarioRepository.delete(scenarioId) + } + + public getScenarioSteps = async (scenarioId: string): Promise => { + return this.scenarioRepository.findAllSteps(scenarioId) + } + + public getScenarioStep = async (scenarioId: string, stepId: string): Promise => { + return this.scenarioRepository.findByStepId(scenarioId, stepId) + } + + public createScenarioStep = async (scenarioId: string, step: NewStep): Promise => { + return this.scenarioRepository.createStep(scenarioId, step) + } + + public updateScenarioStep = async (scenarioId: string, stepId: string, step: NewStep): Promise => { + return this.scenarioRepository.updateStep(scenarioId, stepId, step) + } + + public deleteScenarioStep = async (scenarioId: string, stepId: string): Promise => { + return this.scenarioRepository.deleteStep(scenarioId, stepId) + } + + public getScenarioStepActions = async (scenarioId: string, stepId: string): Promise => { + return this.scenarioRepository.findAllStepActions(scenarioId, stepId) + } + + public getScenarioStepAction = async (scenarioId: string, stepId: string, actionId: string): Promise => { + return this.scenarioRepository.findByStepActionId(scenarioId, stepId, actionId) + } + + public createScenarioStepAction = async (scenarioId: string, stepId: string, action: NewStepActionTypes): Promise => { + return this.scenarioRepository.createStepAction(scenarioId, stepId, action) + } + + public updateScenarioStepAction = async ( + scenarioId: string, + stepId: string, + actionId: string, + action: NewStepActionTypes, + ): Promise => { + return this.scenarioRepository.updateStepAction(scenarioId, stepId, actionId, action) + } + + public deleteScenarioStepAction = async (scenarioId: string, stepId: string, actionId: string): Promise => { + return this.scenarioRepository.deleteStepAction(scenarioId, stepId, actionId) + } + + public getIdBySlug = async (slug: string): Promise => { + return this.scenarioRepository.findIdBySlug(slug) + } +} + +export default ScenarioService diff --git a/apps/bc-wallet-api-server/src/services/ShowcaseService.ts b/apps/bc-wallet-api-server/src/services/ShowcaseService.ts new file mode 100644 index 000000000..d950046c9 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/ShowcaseService.ts @@ -0,0 +1,145 @@ +import { BadRequestError, ForbiddenError, InternalServerError } from 'routing-controllers' +import { Inject, Service } from 'typedi' + +import ShowcaseRepository from '../database/repositories/ShowcaseRepository' +import { NewShowcase, Showcase } from '../types' +import { ISessionService } from '../types/services/session' +import CredentialDefinitionService from './CredentialDefinitionService' + +@Service() +class ShowcaseService { + public constructor( + private readonly showcaseRepository: ShowcaseRepository, + private credentialDefinitionService: CredentialDefinitionService, + @Inject('ISessionService') private readonly sessionService: ISessionService, + ) {} + + public getShowcases = async (): Promise => { + return this.showcaseRepository.findAll(this.getTenantId()) + } + + public getUnapproved() { + return this.showcaseRepository.findUnapproved(this.getTenantId()) + } + + public getShowcase = async (id: string): Promise => { + return this.showcaseRepository.findById(id) + } + + public createShowcase = async (showcase: NewShowcase): Promise => { + const tenantId = this.getTenantId() + if (!showcase.tenantId) { + showcase.tenantId = tenantId + } else if (showcase.tenantId !== tenantId) { + throw new ForbiddenError( + `The showcase is being created for tenant ${showcase.tenantId}, but the signed in tenant is ${tenantId}.`, + ) + } + return this.showcaseRepository.create(showcase) + } + + public updateShowcase = async (id: string, showcase: NewShowcase): Promise => { + const tenantId = this.getTenantId() + if (!showcase.tenantId) { + showcase.tenantId = tenantId + } else if (showcase.tenantId !== tenantId) { + throw new ForbiddenError( + `The showcase is being updated for tenant ${showcase.tenantId}, but the signed in tenant is ${tenantId}.`, + ) + } + + return this.showcaseRepository.update(id, showcase) + } + + public deleteShowcase = async (id: string): Promise => { + const tenantId = this.getTenantId() + const existingShowcase = await this.showcaseRepository.findById(id) + if (existingShowcase) { + if (existingShowcase.tenantId !== tenantId) + return Promise.reject( + new ForbiddenError( + `The showcase is being deleted for tenant ${existingShowcase.tenantId}, but the signed in tenant is ${tenantId}.`, + ), + ) + } else { + return + } + + return this.showcaseRepository.delete(id) + } + + public getIdBySlug = async (slug: string): Promise => { + const tenantId = this.getTenantId() + return this.showcaseRepository.findIdBySlug(slug, tenantId) + } + + /** + * Approves a specific showcase by its ID. + * @param id The ID of the showcase to approve. + * @returns The updated Showcase object. + * @throws NotFoundError if the showcase doesn't exist. + * @throws Error if the current user cannot be determined (implementation specific). + */ + public approveShowcase = async (id: string): Promise => { + const currentUser = await this.sessionService.getCurrentUser() + if (!currentUser) { + return Promise.reject(new Error('Could not determine the approving user.')) + } + const tenantId = this.getTenantId() + return this.showcaseRepository.approve(id, tenantId, currentUser.id) + } + + /** + * Approves a specific showcase identified by its slug. + * This is useful if your controller primarily uses slugs. + * @param slug The slug of the showcase to approve. + * @returns The updated Showcase object. + * @throws NotFoundError if the showcase doesn't exist. + * @throws Error if the current user cannot be determined (implementation specific). + */ + public approveShowcaseBySlug = async (slug: string): Promise => { + const currentUser = await this.sessionService.getCurrentUser() + if (!currentUser) { + throw new Error('Could not determine the approving user.') + } + const tenantId = this.getTenantId() + + const showcaseId = await this.showcaseRepository.findIdBySlug(slug, tenantId) + const showCase = await this.showcaseRepository.findById(showcaseId) + + for (const scenario of showCase.scenarios || []) { + for (const step of scenario.steps || []) { + for (const stepAction of step.actions || []) { + if ( + 'credentialDefinitionId' in stepAction && + stepAction.credentialDefinitionId && + !(await this.credentialDefinitionService.isApproved(stepAction.credentialDefinitionId)) + ) { + return Promise.reject( + new BadRequestError( + `Credential definition id ${stepAction.credentialDefinitionId} used by step ${step.id} / ${step.title} in showcase ${slug} is not approved yet.`, + ), + ) + } + } + } + } + + return this.showcaseRepository.approve(showcaseId, tenantId, currentUser.id) + } + + public isApproved = async (id: string): Promise => { + const showcase = await this.showcaseRepository.findById(id) + return showcase.approvedBy !== undefined && showcase.approvedBy !== null + } + + private getTenantId() { + const urlTenantId = this.sessionService.getUrlTenantId() + if (!urlTenantId) { + throw new InternalServerError('Tenant details are missing') + } + return urlTenantId + } +} + +export default ShowcaseService diff --git a/apps/bc-wallet-api-server/src/services/TenantService.ts b/apps/bc-wallet-api-server/src/services/TenantService.ts new file mode 100644 index 000000000..d14329ec1 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/TenantService.ts @@ -0,0 +1,243 @@ +import { decryptString, encryptString } from 'bc-wallet-adapter-client-api' +import * as process from 'node:process' +import { HttpError, InternalServerError } from 'routing-controllers' +import { Inject, Service } from 'typedi' + +import IssuerRepository from '../database/repositories/IssuerRepository' +import RelyingPartyRepository from '../database/repositories/RelyingPartyRepository' +import TenantRepository from '../database/repositories/TenantRepository' +import { IssuerType, NewTenant, RelyingPartyType, Tenant, TenantType } from '../types' +import { ISessionService } from '../types/services/session' + +const oidcIssuer = process.env.OIDC_ROOT_ISSUER_URL! +const oidcClientId = process.env.OIDC_ROOT_CLIENT_ID! +const oidcClientSecret = process.env.OIDC_ROOT_CLIENT_SECRET! +if (!oidcIssuer || !oidcClientId || !oidcClientSecret) { + throw new Error('OIDC_ROOT_ISSUER_URL, OIDC_ROOT_CLIENT_ID and OIDC_ROOT_CLIENT_SECRET must be set') +} + +const NONCE_SIZE = parseInt(process.env.NONCE_SIZE || '12') || 12 + +@Service() +class TenantService { + public constructor( + private readonly tenantRepository: TenantRepository, + private readonly issuerRepository: IssuerRepository, + private readonly relyingPartyRepository: RelyingPartyRepository, + @Inject('ISessionService') private readonly sessionService: ISessionService, + ) {} + + public getTenants = async (): Promise => { + const tenants = await this.tenantRepository.findAll() + + // Non-root users can only read oidcIssuer + const currentTenant = this.sessionService.getCurrentTenant() + if (!currentTenant || currentTenant.tenantType !== TenantType.ROOT) { + return Promise.all( + tenants.map(async (tenant) => { + return this.redactedTenantFrom(tenant) + }), + ) + } + + return Promise.all( + tenants.map(async (tenant) => { + if (!process.env.ENCRYPTION_KEY) { + return Promise.reject(new InternalServerError(`No encryption key set: ${process.env.ENCRYPTION_KEY}`)) + } + return { + ...tenant, + ...(tenant.tractionApiKey && { + tractionApiKey: decryptString(tenant.tractionApiKey, tenant.nonceBase64 as string), + }), + } + }), + ) + } + + public getTenant = async (id: string, internal: boolean = false): Promise => { + const tenant = await this.tenantRepository.findById(id) + + // Ensure default issuer and relying party exist + await this.ensureDefaultIssuer(tenant) + await this.ensureDefaultRelyingParty(tenant) + + // Non-root users can only read oidcIssuer + const currentTenant = this.sessionService.getCurrentTenant() + if (!internal && (!currentTenant || currentTenant.tenantType !== TenantType.ROOT)) { + return this.redactedTenantFrom(tenant) + } + + if (!process.env.ENCRYPTION_KEY) { + return Promise.reject(new InternalServerError(`No encryption key set: ${process.env.ENCRYPTION_KEY}`)) + } + + return { + ...tenant, + ...(tenant.tractionApiKey && { + tractionApiKey: decryptString(tenant.tractionApiKey, tenant.nonceBase64 as string), + }), + } + } + + public getTenantByIssuerAndClientId = async (issuer: string, clientId: string): Promise => { + if (!process.env.ENCRYPTION_KEY) { + return Promise.reject(new InternalServerError(`No encryption key set: ${process.env.ENCRYPTION_KEY}`)) + } + const tenant = await this.tenantRepository.findByIssuerAndClientId(issuer, clientId) + + // Ensure default issuer and relying party exist + await this.ensureDefaultIssuer(tenant) + await this.ensureDefaultRelyingParty(tenant) + + return { + ...tenant, + ...(tenant.tractionApiKey && { + tractionApiKey: decryptString(tenant.tractionApiKey, tenant.nonceBase64 as string), + }), + } + } + + public createTenant = async (tenant: NewTenant): Promise => { + if (!tenant.tractionApiKey) { + const createdTenant = await this.tenantRepository.create(tenant) + + // Create default issuer and relying party + await this.ensureDefaultIssuer(createdTenant) + await this.ensureDefaultRelyingParty(createdTenant) + + return createdTenant + } else { + const { encryptedApiKeyBase64, nonceBase64 } = encryptString(tenant.tractionApiKey, NONCE_SIZE) // FIXME oidcClientSecret too! + const newTenant = { + ...tenant, + tractionApiKey: encryptedApiKeyBase64, + nonceBase64, + } + const createdTenant = await this.tenantRepository.create(newTenant) + + // Create default issuer and relying party + await this.ensureDefaultIssuer(createdTenant) + await this.ensureDefaultRelyingParty(createdTenant) + + return createdTenant + } + } + + public updateTenant = async (id: string, tenant: NewTenant): Promise => { + try { + const currentTenant = await this.tenantRepository.findById(id) + + const updateData = { + ...tenant, + id: currentTenant.id, + tenantType: currentTenant.tenantType, + } + + if (!tenant.tractionApiKey) { + return this.tenantRepository.update(id, updateData) + } else { + const { encryptedApiKeyBase64, nonceBase64 } = encryptString(tenant.tractionApiKey, NONCE_SIZE) + const newTenant = { + ...updateData, + tractionApiKey: encryptedApiKeyBase64, + nonceBase64, + } + return this.tenantRepository.update(id, newTenant) + } + } catch (error) { + return Promise.reject(new InternalServerError(`Error updating tenant: ${error}`)) + } + } + + public deleteTenant = async (id: string): Promise => { + return this.tenantRepository.delete(id) + } + + public async createRootTenant() { + try { + const rootTenant = await this.getTenantByIssuerAndClientId(oidcIssuer, oidcClientId) + if (rootTenant.tenantType !== TenantType.ROOT) { + return Promise.reject(Error('Configured root tenant is not actually a root tenant')) + } + return rootTenant // Return the existing tenant instead of undefined + } catch (e) { + if (e instanceof HttpError && e.httpCode === 404) { + // Fixed type check syntax + const newRootTenant: NewTenant = { + id: oidcClientId, + tenantType: TenantType.ROOT, + oidcIssuer: oidcIssuer, + } + return this.tenantRepository.create(newRootTenant) + } else { + return Promise.reject(e) + } + } + } + + private redactedTenantFrom(tenant: Tenant) { + return { + id: tenant.id, + tenantType: tenant.tenantType, + oidcIssuer: tenant.oidcIssuer, + createdAt: tenant.createdAt, + updatedAt: tenant.updatedAt, + deletedAt: tenant.deletedAt, + issuers: tenant.issuers, + relyingParties: tenant.relyingParties, + tractionTenantId: null, + tractionApiUrl: null, + tractionWalletId: null, + tractionApiKey: null, + nonceBase64: null, + } satisfies Tenant + } + + private async ensureDefaultIssuer(tenant: Tenant): Promise { + // Check if tenant already has an issuer + if (tenant.issuers && tenant.issuers.length > 0) { + return + } + + // Create default issuer + const newIssuer = await this.issuerRepository.create({ + name: `Default Issuer for ${tenant.id}`, + type: IssuerType.ARIES, + description: 'Default issuer created automatically', + credentialDefinitions: [], + credentialSchemas: [], + tenantId: tenant.id, + }) + + // Also add the new issuer to the tenant object + if (!tenant.issuers) { + tenant.issuers = [] + } + tenant.issuers.push(newIssuer) + } + + private async ensureDefaultRelyingParty(tenant: Tenant): Promise { + // Check if tenant already has a relying party + if (tenant.relyingParties && tenant.relyingParties.length > 0) { + return + } + const RelyingParty = process.env.RELYING_PARTY_NAME + // Create default relying party + const newRelyingParty = await this.relyingPartyRepository.create({ + name: RelyingParty ? `${RelyingParty} for ${tenant.id}` : `Default Relying Party for ${tenant.id}`, + type: RelyingPartyType.ARIES, + description: 'Default relying party created automatically', + credentialDefinitions: [], + tenantId: tenant.id, + }) + + // Also add the new relying party to the tenant object + if (!tenant.relyingParties) { + tenant.relyingParties = [] + } + tenant.relyingParties.push(newRelyingParty) + } +} + +export default TenantService diff --git a/apps/bc-wallet-api-server/src/services/UserService.ts b/apps/bc-wallet-api-server/src/services/UserService.ts new file mode 100644 index 000000000..24b58eaf7 --- /dev/null +++ b/apps/bc-wallet-api-server/src/services/UserService.ts @@ -0,0 +1,46 @@ +import { Service } from 'typedi' + +import { UserRepository } from '../database/repositories/UserRepository' +import type { NewUser, User } from '../types' + +@Service() +class UserService { + public constructor(private readonly userRepository: UserRepository) {} + + public getUsers = async (): Promise => { + return this.userRepository.findAll() + } + + public getUser = async (id: string): Promise => { + return this.userRepository.findById(id) + } + + public getUserByNameAndTenantId = async (userName: string, tenantId: string): Promise => { + return this.userRepository.findByUsernameAndTenantId(userName, tenantId) + } + + public createUser = async (user: NewUser): Promise => { + return this.userRepository.create(user) + } + + public updateUser = async (id: string, user: NewUser): Promise => { + return this.userRepository.update(id, user) + } + + public deleteUser = async (id: string): Promise => { + return this.userRepository.delete(id) + } + + public insertIfNotExists = async (user: NewUser) => { + if (!user.id) { + throw new Error('User id is required') + } + try { + return await this.userRepository.findById(user.id) + } catch (e) { + return await this.userRepository.create(user) + } + } +} + +export default UserService diff --git a/apps/bc-wallet-api-server/src/types/auth/claims.ts b/apps/bc-wallet-api-server/src/types/auth/claims.ts new file mode 100644 index 000000000..c852fe46e --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/auth/claims.ts @@ -0,0 +1,39 @@ +export interface Claims { + // Standard JWT claims + exp: number + iat: number + auth_time: number + jti: string + iss: string + aud: string[] | string + sub: string + typ: string + + // OpenID and auth specific + azp: string + session_state: string + acr: string + sid: string + scope: string + + // Custom claims + 'allowed-origins'?: string[] + realm_access?: { + roles: string[] + } + resource_access?: { + [key: string]: { + roles: string[] + } + } + + // User info + email_verified?: boolean + name?: string + preferred_username?: string + given_name?: string + email?: string + + // Additional properties + [key: string]: any +} diff --git a/apps/bc-wallet-api-server/src/types/auth/session.ts b/apps/bc-wallet-api-server/src/types/auth/session.ts new file mode 100644 index 000000000..0b4710f7c --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/auth/session.ts @@ -0,0 +1,12 @@ +import { Tenant, User } from '../schema' +import { Claims } from './claims' +import { Token } from './token' + +export interface OidcSession { + user: User | null + tenant: Tenant | null + urlTenantId: string | null + bearerToken?: Token + apiBaseUrl?: string + activeClaims?: Claims +} diff --git a/apps/bc-wallet-api-server/src/types/auth/token.ts b/apps/bc-wallet-api-server/src/types/auth/token.ts new file mode 100644 index 000000000..65700fb27 --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/auth/token.ts @@ -0,0 +1,92 @@ +import { createHash } from 'crypto' +import { UnauthorizedError } from 'routing-controllers' + +import { Claims } from './claims' + +export class Token { + private readonly _payload: Claims + private readonly _header: any + private readonly _signature: string + private readonly _rawToken: string + private readonly _parts: string[] + + public constructor(authHeader?: string) { + if (!authHeader) { + throw new UnauthorizedError('Missing or malformed Authorization header / bearer token') + } + + const authHeaderELem = authHeader.split(' ') // Authorization: Bearer + const accessToken = authHeaderELem[authHeaderELem.length - 1] + + this._rawToken = accessToken + this._parts = accessToken.split('.') + + if (this._parts.length !== 3) { + throw new Error('Invalid token string') + } + + try { + this._header = JSON.parse(Buffer.from(this._parts[0], 'base64url').toString()) + this._payload = JSON.parse(Buffer.from(this._parts[1], 'base64url').toString()) + this._signature = this._parts[2] + } catch (error) { + throw new Error(`Error parsing JWT: ${error.message}`) + } + } + + public get payload(): Claims { + return this._payload + } + + public get header(): any { + return this._header + } + + public getSignature(): string { + return this._signature + } + + public getRawToken(): string { + return this._rawToken + } + + public getSignatureHash(): string { + return createHash('sha256').update(this._signature).digest('hex') + } + + public hasRole = (name: string) => { + if (!this._payload?.azp) { + return false + } + const parts = name.split(':') + if (parts.length === 1) { + return this.hasApplicationRole(this._payload?.azp, parts[0]) + } + if (parts[0] === 'realm') { + return this.hasRealmRole(parts[1]) + } + return this.hasApplicationRole(parts[0], parts[1]) + } + + public hasApplicationRole = (appName: string, roleName: string): boolean => { + if (!this._payload.resource_access) { + return false + } + + const appRoles = this._payload.resource_access[appName] + + if (!appRoles) { + return false + } + + return appRoles.roles.indexOf(roleName) >= 0 + } + + public hasRealmRole = (roleName: string) => { + if (!this._payload.realm_access || !this._payload.realm_access.roles) { + return false + } + + return this._payload.realm_access.roles.indexOf(roleName) >= 0 + } +} diff --git a/apps/bc-wallet-api-server/src/types/index.ts b/apps/bc-wallet-api-server/src/types/index.ts new file mode 100644 index 000000000..db9b86337 --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/index.ts @@ -0,0 +1,4 @@ +export * from './respository' +export * from './schema' +export * from './scenario' +export * from './slug' diff --git a/apps/bc-wallet-api-server/src/types/respository/index.ts b/apps/bc-wallet-api-server/src/types/respository/index.ts new file mode 100644 index 000000000..4df3548f5 --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/respository/index.ts @@ -0,0 +1,23 @@ +import { ExtractTablesWithRelations } from 'drizzle-orm' +import { NodePgQueryResultHKT } from 'drizzle-orm/node-postgres' +import { PgTransaction } from 'drizzle-orm/pg-core' + +import * as schema from '../../database/schema' + +export type RepositoryDefinition = { + findById(id: string): Promise + findAll(filter?: Record): Promise + create(item: U): Promise + update(id: string, item: U): Promise + delete(id: string): Promise +} + +export type TenantScopedRepositoryDefinition = { + findById(id: string, tenantId?: string): Promise + findAll(tenantId: string): Promise + create(item: U): Promise + update(id: string, item: U): Promise + delete(id: string): Promise +} + +export type Tx = PgTransaction> diff --git a/apps/bc-wallet-api-server/src/types/scenario/index.ts b/apps/bc-wallet-api-server/src/types/scenario/index.ts new file mode 100644 index 000000000..6d0d9dd06 --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/scenario/index.ts @@ -0,0 +1,9 @@ +import { ScenarioType } from '../schema' + +export type ScenarioFindAllArgs = { + filter: ScenarioFilter +} + +export type ScenarioFilter = { + scenarioType: ScenarioType +} diff --git a/apps/bc-wallet-api-server/src/types/schema/index.ts b/apps/bc-wallet-api-server/src/types/schema/index.ts new file mode 100644 index 000000000..37db983ed --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/schema/index.ts @@ -0,0 +1,344 @@ +import { + ariesProofRequests, + assets, + credentialAttributes, + credentialDefinitions, + credentialRepresentations, + credentialSchemas, + issuers, + personas, + relyingParties, + revocationInfo, + scenarios, + showcases, + stepActions, + steps, + tenants, + users, +} from '../../database/schema' + +// $inferSelect does not respect nullability of fields and the type has every field as required +// $inferInsert only provides fields that are notNull() or generated by default. This seems to be a bug in DrizzleORM or very not pleasant choice + +export type User = typeof users.$inferSelect & { + tenants?: Tenant[] +} +export type NewUser = typeof users.$inferInsert & { + // NewUser allows id to be preset + id?: string | null + userName?: string | null + issuer?: string | null + clientId?: string | null + tenants?: string[] +} + +export type Asset = typeof assets.$inferSelect +export type NewAsset = typeof assets.$inferInsert & { fileName?: string | null; description?: string | null } + +export type Persona = Omit & { + headshotImage: Asset | null + bodyImage: Asset | null +} +export type NewPersona = Omit & { + headshotImage?: string | null + bodyImage?: string | null + hidden: boolean +} + +export type CredentialDefinition = Omit< + typeof credentialDefinitions.$inferSelect, + 'icon' | 'type' | 'credentialSchema' | 'approvedBy' +> & { + type: CredentialType + icon?: Asset + credentialSchema: CredentialSchema + representations: CredentialRepresentation[] + revocation?: RevocationInfo | null + approvedBy?: User | null + approvedAt?: Date | null +} + +export type NewCredentialDefinition = Omit & { + type: CredentialType + icon?: string | null + representations?: NewCredentialRepresentation[] // TODO SHOWCASE-81 make required + revocation?: NewRevocationInfo | null + identifierType?: IdentifierType | null + identifier?: string | null +} + +export type CredentialAttribute = Omit & { + type: CredentialAttributeType +} +export type NewCredentialAttribute = Omit & { + type: CredentialAttributeType +} + +export type CredentialSchema = typeof credentialSchemas.$inferSelect & { + attributes: CredentialAttribute[] +} + +export type NewCredentialSchema = typeof credentialSchemas.$inferInsert & { + attributes: NewCredentialAttribute[] + identifierType?: IdentifierType | null + identifier?: string | null +} + +export type CredentialRepresentation = Omit +export type NewCredentialRepresentation = Omit + +export type RevocationInfo = Omit +export type NewRevocationInfo = Omit + +export type RelyingParty = Omit & { + credentialDefinitions: CredentialDefinition[] + logo: Asset | null +} +export type NewRelyingParty = Omit & { + credentialDefinitions: string[] + organization?: string | null + logo?: string | null + tenantId: string +} + +export type Issuer = Omit & { + credentialDefinitions: CredentialDefinition[] + credentialSchemas: CredentialSchema[] + logo: Asset | null +} +export type NewIssuer = Omit & { + credentialDefinitions: string[] + credentialSchemas: string[] + organization?: string | null + logo?: string | null + tenantId: string +} + +export enum CredentialType { + ANONCRED = 'ANONCRED', +} + +export enum CredentialAttributeType { + STRING = 'STRING', + INTEGER = 'INTEGER', + FLOAT = 'FLOAT', + BOOLEAN = 'BOOLEAN', + DATE = 'DATE', +} + +export enum RelyingPartyType { + ARIES = 'ARIES', +} + +export enum IdentifierType { + DID = 'DID', +} + +export enum Source { + IMPORTED = 'IMPORTED', + CREATED = 'CREATED', +} + +export enum IssuerType { + ARIES = 'ARIES', +} + +export enum StepType { + HUMAN_TASK = 'HUMAN_TASK', + SERVICE = 'SERVICE', + SCENARIO = 'SCENARIO', +} + +export enum StepActionType { + ARIES_OOB = 'ARIES_OOB', + ACCEPT_CREDENTIAL = 'ACCEPT_CREDENTIAL', + SETUP_CONNECTION = 'SETUP_CONNECTION', + CHOOSE_WALLET = 'CHOOSE_WALLET', + BUTTON = 'BUTTON', +} + +export enum ScenarioType { + ISSUANCE = 'ISSUANCE', + PRESENTATION = 'PRESENTATION', +} + +export type IssuanceScenario = Omit & { + personas: Persona[] + steps: Step[] + issuer?: Issuer | null + bannerImage?: Asset | null +} +export type NewIssuanceScenario = Omit & { + personas: string[] + issuer: string + steps: NewStep[] + bannerImage?: string | null + hidden: boolean +} + +export type PresentationScenario = Omit & { + personas: Persona[] + steps: Step[] + relyingParty?: RelyingParty | null + bannerImage?: Asset | null +} +export type NewPresentationScenario = Omit & { + personas: string[] + relyingParty: string + steps: NewStep[] + bannerImage?: string | null + hidden: boolean +} + +export type StepActionTypes = + | StepAction + | AcceptCredentialAction + | ButtonAction + | AriesOOBAction + | SetupConnectionAction + | ChooseWalletAction +export type NewStepActionTypes = + | NewStepAction + | NewAcceptCredentialAction + | NewButtonAction + | NewAriesOOBAction + | NewSetupConnectionAction + | NewChooseWalletAction + +export type Step = Omit & { + actions?: StepActionTypes[] + asset?: Asset | null +} +export type NewStep = Omit & { + asset?: string | null + actions?: NewStepActionTypes[] + subScenario?: string | null +} +export type StepAction = typeof stepActions.$inferSelect +export type NewStepAction = Omit + +export type AcceptCredentialAction = Omit & { + actionType: StepActionType.ACCEPT_CREDENTIAL + credentialDefinitionId: string + connectionId?: string | null +} +export type NewAcceptCredentialAction = Omit & { + actionType: StepActionType.ACCEPT_CREDENTIAL + credentialDefinitionId: string + connectionId?: string | null +} + +export type ButtonAction = Omit & { + actionType: StepActionType.BUTTON + goToStep?: string | null +} +export type NewButtonAction = Omit< + typeof stepActions.$inferInsert, + 'step' | 'credentialDefinitionId' | 'connectionId' +> & { + actionType: StepActionType.BUTTON + goToStep?: string | null +} + +export type AriesOOBAction = Omit & { + actionType: StepActionType.ARIES_OOB + credentialDefinitionId: string + proofRequest?: AriesProofRequest | null +} +export type NewAriesOOBAction = Omit & { + actionType: StepActionType.ARIES_OOB + proofRequest: NewAriesProofRequest +} + +export type SetupConnectionAction = Omit< + typeof stepActions.$inferSelect, + 'credentialDefinitionId' | 'connectionId' | 'goToStep' +> & { + actionType: StepActionType.SETUP_CONNECTION +} +export type NewSetupConnectionAction = Omit< + typeof stepActions.$inferInsert, + 'step' | 'credentialDefinitionId' | 'connectionId' | 'goToStep' +> & { + actionType: StepActionType.SETUP_CONNECTION +} + +export type ChooseWalletAction = Omit< + typeof stepActions.$inferSelect, + 'credentialDefinitionId' | 'connectionId' | 'goToStep' +> & { + actionType: StepActionType.CHOOSE_WALLET +} +export type NewChooseWalletAction = Omit< + typeof stepActions.$inferInsert, + 'step' | 'credentialDefinitionId' | 'connectionId' | 'goToStep' +> & { + actionType: StepActionType.CHOOSE_WALLET +} + +export type AriesRequestCredentialAttribute = { + attributes?: string[] + restrictions?: string[] +} + +export type AriesRequestCredentialPredicate = { + name?: string + type?: string + value?: number + restrictions?: string[] +} + +export type AriesRequestPredicates = { + predicates?: AriesRequestCredentialPredicate[] +} + +export type AriesProofRequest = typeof ariesProofRequests.$inferSelect +export type NewAriesProofRequest = Omit & { + attributes?: Record + predicates?: Record +} + +export type Tenant = typeof tenants.$inferSelect & { + users?: User[] + issuers?: Issuer[] + relyingParties?: RelyingParty[] +} + +export type NewTenant = typeof tenants.$inferInsert & { + users?: string[] + issuers?: string[] + relyingParties?: string[] +} + +export type Showcase = Omit & { + scenarios: Scenario[] + personas: Persona[] + bannerImage?: Asset | null + createdBy?: User | null + approvedBy?: User | null + approvedAt?: Date | null +} + +export type NewShowcase = Omit & { + scenarios: string[] + credentialDefinitions?: string[] // No longer in use + personas: string[] + bannerImage?: string | null + hidden: boolean + completionMessage?: string | null + createdBy?: string | null +} + +export type Scenario = IssuanceScenario | PresentationScenario +export type NewScenario = NewIssuanceScenario | NewPresentationScenario + +export enum ShowcaseStatus { + PENDING = 'PENDING', + ACTIVE = 'ACTIVE', + ARCHIVED = 'ARCHIVED', +} + +export enum TenantType { + ROOT = 'ROOT', + SHOWCASE = 'SHOWCASE', +} diff --git a/apps/bc-wallet-api-server/src/types/services/session.ts b/apps/bc-wallet-api-server/src/types/services/session.ts new file mode 100644 index 000000000..3723ca5c6 --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/services/session.ts @@ -0,0 +1,34 @@ +import { Claims } from '../auth/claims' +import type { OidcSession } from '../auth/session' +import { Token } from '../auth/token' +import type { Tenant, User } from '../schema' + +export interface ISessionService { + getCurrentUser(): User | null + + getCurrentTenant(): Tenant | null + + getUrlTenantId(): string | null + + getBearerToken(): Token | undefined + + getApiBaseUrl(): string | undefined + + getActiveClaims(): Claims | undefined + + getCachedSessionByTokenHash(tokenHash: string): OidcSession | undefined +} + +export interface ISessionServiceUpdater extends ISessionService { + setRequestDetails(apiBaseUrl: string, tenantId: string | null, token?: Token): void + + setActiveClaims(claims: Claims): void + + setCurrentTenant(value: Tenant): void + + setCurrentUser(userName: string): Promise + + cacheValidatedToken(tokenHash: string, token: Token, claims: Claims, ttlMs: number): void + + clear(): void +} diff --git a/apps/bc-wallet-api-server/src/types/slug/index.ts b/apps/bc-wallet-api-server/src/types/slug/index.ts new file mode 100644 index 000000000..a6cf37a9e --- /dev/null +++ b/apps/bc-wallet-api-server/src/types/slug/index.ts @@ -0,0 +1,12 @@ +import { NodePgDatabase } from 'drizzle-orm/node-postgres' +import { personas, scenarios, showcases } from '../../database/schema' +import * as schema from '../../database/schema' + +export type Schemas = typeof showcases | typeof personas | typeof scenarios + +export type GenerateSlugArgs = { + value: string + id?: string + connection: NodePgDatabase + schema: Schemas +} diff --git a/apps/bc-wallet-api-server/src/utils/auth.ts b/apps/bc-wallet-api-server/src/utils/auth.ts new file mode 100644 index 000000000..32471ce06 --- /dev/null +++ b/apps/bc-wallet-api-server/src/utils/auth.ts @@ -0,0 +1,226 @@ +import { Buffer } from 'buffer' +import fetch from 'cross-fetch' +import process from 'node:process' +import { Action, BadRequestError, UnauthorizedError } from 'routing-controllers' +import Container from 'typedi' + +import TenantService from '../services/TenantService' +import { Claims } from '../types/auth/claims' +import { Token } from '../types/auth/token' +import { ISessionService, ISessionServiceUpdater } from '../types/services/session' + +// TODO for now we introspect using the root tenant which is fine for the current setup, +// but when we need to support multiple issuer servers / realms for one DB, we need to store multiple client ids & secrets in the tenant record and look them up as well for the introspection calls +const oidcClientId = process.env.OIDC_ROOT_CLIENT_ID +const oidcClientSecret = process.env.OIDC_ROOT_CLIENT_SECRET +if (!oidcClientId || !oidcClientSecret) { + throw new Error('OIDC_ROOT_CLIENT_ID and OIDC_ROOT_CLIENT_SECRET must be set') +} + +export function checkRoles(token: Token, roles: string[]) { + if (token && !roles.length) { + return true + } + return !!(token && roles.find((role) => token.hasRole(role))) +} + +export async function isAccessTokenValid(token: Token, authServerUrl: string): Promise { + const tokenHash = token.getSignatureHash() + const oidcSessionService = Container.get('ISessionService') + const cachedSession = oidcSessionService.getCachedSessionByTokenHash(tokenHash) + + if (cachedSession && cachedSession.bearerToken && cachedSession.activeClaims) { + if (!isAccessTokenExpired(cachedSession.bearerToken)) { + return true + } + } + + const authorization = 'Basic ' + Buffer.from(`${oidcClientId}:${oidcClientSecret}`).toString('base64') + const response = await fetch(`${authServerUrl}/protocol/openid-connect/token/introspect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: authorization, + }, + body: new URLSearchParams({ + token: token.getRawToken(), + client_id: `${oidcClientId}`, + client_secret: `${oidcClientSecret}`, + }), + }) + await checkResponse(response) + const claims = (await response.json()) as Claims + const sessionUpdater = Container.get('ISessionService') + + if (claims.active) { + sessionUpdater.setActiveClaims(claims) + + // Cache the token and claims within the session object with expiration based on the token's exp claim + if (token.payload.exp) { + const expiryMs = token.payload.exp * 1000 - Date.now() + sessionUpdater.cacheValidatedToken(tokenHash, token, claims, expiryMs) + } + return true + } + return false +} + +async function checkResponse(response: Response) { + if (response.status < 400) { + return response + } + + let errorMessage = `HTTP error! Status: ${response.status} (${response.statusText})` + + try { + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + const errorData = await response.json() + errorMessage += `\nDetails: ${JSON.stringify(errorData)}` + } else { + const textContent = await response.text() + if (textContent) { + errorMessage += `\nResponse: ${textContent}` + } + } + } catch (e) { + errorMessage += `\nNo error details available` + } + throw new Error(errorMessage) +} + +export function getBasePath(path?: string): string { + const basePath = path ?? '' + // FIXME mock not working for some reason (see setup-mocks.ts) + if (process.env.NODE_ENV === 'test') { + return `${basePath}` + } + + return `/:tenantId${basePath}` +} + +function determineClientId(token: Token, action: Action | undefined) { + const clientAzp = token.payload.azp + if (!clientAzp) { + throw new BadRequestError('Client ID (azp) is required in token') + } + + if (action) { + const path = action.request.url + const segments = path.split('/').filter(Boolean) + if (segments.length < 2) { + throw new UnauthorizedError(`Invalid request path: ${path} No tenant specified?`) + } + const routeTenant = segments[0] + + // enforce match between URL and token + if (routeTenant !== clientAzp) { + throw new UnauthorizedError(`Tenant mismatch: URL="${routeTenant}" vs token.azp="${clientAzp}"`) + } + + return routeTenant + } else { + return clientAzp + } +} + +async function processAccessToken(token?: Token, action?: Action) { + if (!token) { + throw new UnauthorizedError('Missing authorization / bearer token') + } + const issuerUrl = token.payload.iss + if (!issuerUrl) { + throw new UnauthorizedError('Issuer URL is required in token') + } + const clientId = determineClientId(token, action) + const tenantService = Container.get(TenantService) + + if (!(await isAccessTokenValid(token, issuerUrl))) { + throw new UnauthorizedError('Invalid token') + } + const sessionService: ISessionServiceUpdater = Container.get('ISessionService') + sessionService.setCurrentTenant(await lookupTenant(tenantService, issuerUrl, clientId)) + + const activeClaims = sessionService.getActiveClaims() + if (!activeClaims) { + throw new UnauthorizedError( + 'No active claims found for token, token might be expired or claims not set in session.', + ) + } + if (activeClaims.preferred_username) { + // `void` is used to indicate that we are intentionally not awaiting the promise here. + void sessionService.setCurrentUser(activeClaims.preferred_username) + } + + return token +} + +async function lookupTenant(tenantService: TenantService, issuerUrl: string, clientId: string) { + try { + return await tenantService.getTenantByIssuerAndClientId(issuerUrl, clientId) + } catch (error) { + // TODO: Remove this workaround when the issue is fixed in drizzle-orm: https://4sure.atlassian.net/browse/SHOWCASE-308 + if (error.message.includes('already exists')) { + return await tenantService.getTenantByIssuerAndClientId(issuerUrl, clientId) + } else { + throw new UnauthorizedError(`Tenant not found for ${clientId} @ ${issuerUrl}: ${error.message}`) + } + } +} + +export async function authorizationChecker(action: Action, roles: string[]): Promise { + const authHeader: string | undefined = action.request.headers['authorization'] + const inputToken = authHeader ? new Token(authHeader) : undefined + const token = await processAccessToken(inputToken, action) + + // Realm roles must be prefixed with 'realm:', client roles must be prefixed with the value of clientId + : and + // User roles which at the moment we are not using, do not need any prefix. + return checkRoles(token, roles) +} + +export function isAccessTokenExpired(token: Token): boolean { + const currentTime = Math.floor(Date.now() / 1000) + + if (!token.payload.exp) { + console.warn('Token does not contain an expiration date, assuming it is expired.') + return true + } + + return currentTime > token.payload.exp +} + +export function RootTenantAuthorized() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value + descriptor.value = async function (...args: any[]) { + const sessionService: ISessionServiceUpdater = Container.get('ISessionService') + + const bearerToken = sessionService.getBearerToken() + void (await processAccessToken(bearerToken)) + return originalMethod.apply(this, args) + } + return descriptor + } +} + +export function SoftTenantAuthorized() { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value + descriptor.value = async function (...args: any[]) { + const sessionService: ISessionServiceUpdater = Container.get('ISessionService') + + try { + const bearerToken = sessionService.getBearerToken() + if (bearerToken) { + await processAccessToken(bearerToken) + } + } catch (error) { + // ignore bad token, just clear the session vars + sessionService.clear() + } + + return originalMethod.apply(this, args) + } + return descriptor + } +} diff --git a/apps/bc-wallet-api-server/src/utils/cors.ts b/apps/bc-wallet-api-server/src/utils/cors.ts new file mode 100644 index 000000000..d47163b03 --- /dev/null +++ b/apps/bc-wallet-api-server/src/utils/cors.ts @@ -0,0 +1,36 @@ +import type { CorsOptions } from 'cors' + +export const corsOptions: CorsOptions = { + origin: (requestOrigin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { + const origins = process.env.CORS_ALLOW_ORIGINS?.replace(/\s+/g, '').split(/[,;]/).filter(Boolean) ?? ['*'] + + if (!requestOrigin || process.env.CORS_DISABLED === 'true') return callback(null, true) + if (origins.includes('*') || origins.includes(requestOrigin)) { + return callback(null, true) + } + const msg = 'The CORS policy for this site does not allow access from the specified Origin.' + return callback(new Error(msg), false) + }, + methods: (() => { + const methods = process.env.CORS_ALLOW_METHODS?.replace(/\s+/g, '').split(/[,;]/).filter(Boolean) ?? [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'OPTIONS', + ] + return methods.includes('*') ? '*' : methods + })(), + allowedHeaders: (() => { + const headers = process.env.CORS_ALLOW_HEADERS?.replace(/\s+/g, '').split(/[,;]/).filter(Boolean) ?? [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'Cache-Control', + 'Pragma', + 'Expires', + ] + return headers.includes('*') ? '*' : headers + })(), + credentials: process.env.CORS_ALLOW_CREDENTIALS === 'true', +} diff --git a/apps/bc-wallet-api-server/src/utils/mappers.ts b/apps/bc-wallet-api-server/src/utils/mappers.ts new file mode 100644 index 000000000..1b92e5553 --- /dev/null +++ b/apps/bc-wallet-api-server/src/utils/mappers.ts @@ -0,0 +1,248 @@ +import { + AcceptCredentialAction as AcceptCredentialActionDTO, + AriesOOBAction as AriesOOBActionDTO, + AriesProofRequest as AriesProofRequestDTO, + Asset as AssetDTO, + AssetRequest as AssetRequestDTO, + ButtonAction as ButtonActionDTO, + ChooseWalletAction as ChooseWalletActionDTO, + CredentialDefinition as CredentialDefinitionDTO, + CredentialSchema as CredentialSchemaDTO, + IssuanceScenario as IssuanceScenarioDTO, + Issuer as IssuerDTO, + Persona as PersonaDTO, + PresentationScenario as PresentationScenarioDTO, + RelyingParty as RelyingPartyDTO, + SetupConnectionAction as SetupConnectionActionDTO, + Showcase as ShowcaseDTO, + Step as StepDTO, + StepAction as StepActionDTO, + User as UserDTO, +} from 'bc-wallet-openapi' + +import { + AcceptCredentialAction, + AriesOOBAction, + Asset, + ButtonAction, + CredentialDefinition, + CredentialSchema, + IssuanceScenario, + Issuer, + NewAsset, + NewScenario, + Persona, + PresentationScenario, + RelyingParty, + Scenario, + ScenarioType, + Showcase, + Step, + StepActionType, + StepActionTypes, + User, +} from '../types' + +export const newAssetFrom = (asset: AssetRequestDTO): NewAsset => { + return { + ...asset, + content: Buffer.from(asset.content), + } +} + +export const assetDTOFrom = (asset: Asset): AssetDTO => { + return { + ...asset, + fileName: asset.fileName || undefined, + description: asset.description || undefined, + content: '', // remove the content field + } +} + +export const credentialSchemaDTOFrom = (credentialSchema: CredentialSchema): CredentialSchemaDTO => { + return { + ...credentialSchema, + identifierType: credentialSchema.identifierType || undefined, + identifier: credentialSchema.identifier || undefined, + source: credentialSchema.source || undefined, + } +} + +export const credentialDefinitionDTOFrom = (credentialDefinition: CredentialDefinition): CredentialDefinitionDTO => { + return { + ...credentialDefinition, + identifierType: credentialDefinition.identifierType || undefined, + identifier: credentialDefinition.identifier || undefined, + source: credentialDefinition.source || undefined, + name: credentialSchemaDTOFrom(credentialDefinition.credentialSchema).name || credentialDefinition.name, + credentialSchema: credentialSchemaDTOFrom(credentialDefinition.credentialSchema), + representations: credentialDefinition.representations, + revocation: credentialDefinition.revocation || undefined, + icon: credentialDefinition.icon ? assetDTOFrom(credentialDefinition.icon) : undefined, + approvedBy: credentialDefinition.approvedBy ? userDTOFrom(credentialDefinition.approvedBy) : undefined, + approvedAt: credentialDefinition.approvedAt ?? undefined, + } +} + +export const relyingPartyDTOFrom = (relyingParty: RelyingParty): RelyingPartyDTO => { + return { + ...relyingParty, + organization: relyingParty.organization || undefined, + logo: relyingParty.logo ? assetDTOFrom(relyingParty.logo) : undefined, + credentialDefinitions: relyingParty.credentialDefinitions.map(credentialDefinitionDTOFrom), + } +} + +export const issuerDTOFrom = (issuer: Issuer): IssuerDTO => { + return { + ...issuer, + organization: issuer.organization || undefined, + logo: issuer.logo ? assetDTOFrom(issuer.logo) : undefined, + credentialDefinitions: issuer.credentialDefinitions.map(credentialDefinitionDTOFrom), + credentialSchemas: issuer.credentialSchemas.map(credentialSchemaDTOFrom), + } +} + +export const issuanceScenarioDTOFrom = (issuanceScenario: IssuanceScenario): IssuanceScenarioDTO => { + if (!issuanceScenario.issuer) { + throw Error('Missing issuer in issuance scenario') + } + + return { + ...issuanceScenario, + issuer: issuerDTOFrom(issuanceScenario.issuer), + type: ScenarioType.ISSUANCE, + steps: issuanceScenario.steps.map(stepDTOFrom), + personas: issuanceScenario.personas.map(personaDTOFrom), + } +} + +export const presentationScenarioDTOFrom = (presentationScenario: PresentationScenario): PresentationScenarioDTO => { + if (!presentationScenario.relyingParty) { + throw Error('Missing relying party in presentation scenario') + } + + return { + ...presentationScenario, + relyingParty: relyingPartyDTOFrom(presentationScenario.relyingParty), + type: ScenarioType.PRESENTATION, + steps: presentationScenario.steps.map(stepDTOFrom), + personas: presentationScenario.personas.map(personaDTOFrom), + } +} + +export const scenarioDTOFrom = (scenario: Scenario): IssuanceScenarioDTO | PresentationScenarioDTO => { + switch (scenario.scenarioType) { + case ScenarioType.PRESENTATION: + return presentationScenarioDTOFrom(scenario as PresentationScenario) + case ScenarioType.ISSUANCE: + return issuanceScenarioDTOFrom(scenario as IssuanceScenario) + default: + throw Error(`Unsupported scenario type ${scenario.scenarioType}`) + } +} + +export const stepActionDTOFrom = (stepAction: StepActionTypes): StepActionDTO => { + const baseAction = { + id: stepAction.id, + actionType: stepAction.actionType, + title: stepAction.title, + text: stepAction.text, + createdAt: stepAction.createdAt, + updatedAt: stepAction.updatedAt, + } + + // Handle specific action types with their unique properties + switch (stepAction.actionType) { + case StepActionType.ACCEPT_CREDENTIAL: { + const action = stepAction as AcceptCredentialAction + const acceptCredentialDTO: AcceptCredentialActionDTO = { + ...baseAction, + credentialDefinitionId: action.credentialDefinitionId, + connectionId: action.connectionId || undefined, + } + return acceptCredentialDTO + } + case StepActionType.BUTTON: { + const action = stepAction as ButtonAction + const buttonDTO: ButtonActionDTO = { + ...baseAction, + goToStep: action.goToStep || undefined, + } + return buttonDTO + } + case StepActionType.ARIES_OOB: { + const action = stepAction as AriesOOBAction + const ariesOOBDTO: AriesOOBActionDTO = { + ...baseAction, + credentialDefinitionId: action.credentialDefinitionId, + proofRequest: action.proofRequest as AriesProofRequestDTO, + } + return ariesOOBDTO + } + case StepActionType.CHOOSE_WALLET: { + const chooseWalletDTO: ChooseWalletActionDTO = { + ...baseAction, + } + return chooseWalletDTO + } + case StepActionType.SETUP_CONNECTION: { + const setupConnectionDTO: SetupConnectionActionDTO = { + ...baseAction, + } + return setupConnectionDTO + } + default: + throw Error(`Unknown step action action type: ${stepAction['actionType']}`) + } +} + +export const stepDTOFrom = (step: Step): StepDTO => { + return { + ...step, + actions: step.actions ? step.actions.map(stepActionDTOFrom) : [], + asset: step.asset ? assetDTOFrom(step.asset) : undefined, + subScenario: step.subScenario || undefined, + } +} + +export const personaDTOFrom = (persona: Persona): PersonaDTO => { + return { + ...persona, + headshotImage: persona.headshotImage ? assetDTOFrom(persona.headshotImage) : undefined, + bodyImage: persona.bodyImage ? assetDTOFrom(persona.bodyImage) : undefined, + hidden: persona.hidden, + } +} + +export const showcaseDTOFrom = (showcase: Showcase): ShowcaseDTO => { + return { + ...showcase, + personas: showcase.personas.map(personaDTOFrom), + scenarios: showcase.scenarios.map(scenarioDTOFrom), + bannerImage: showcase.bannerImage ? assetDTOFrom(showcase.bannerImage) : undefined, + completionMessage: showcase.completionMessage || undefined, + createdBy: showcase.createdBy ? userDTOFrom(showcase.createdBy) : undefined, + approvedBy: showcase.approvedBy ? userDTOFrom(showcase.approvedBy) : undefined, + approvedAt: showcase.approvedAt || undefined, + } +} + +export const userDTOFrom = (user: User): UserDTO => { + return { + ...user, + userName: user.userName ?? undefined, + issuer: user.issuer ?? undefined, + clientId: user.clientId ?? undefined, + createdAt: user.createdAt ?? undefined, + updatedAt: user.updatedAt ?? undefined, + } +} + +export const isPresentationScenario = (scenario: Scenario | NewScenario): boolean => { + return 'relyingParty' in scenario +} + +export const isIssuanceScenario = (scenario: Scenario | NewScenario): boolean => { + return 'issuer' in scenario +} diff --git a/apps/bc-wallet-api-server/src/utils/slug.ts b/apps/bc-wallet-api-server/src/utils/slug.ts new file mode 100644 index 000000000..739b7c6b2 --- /dev/null +++ b/apps/bc-wallet-api-server/src/utils/slug.ts @@ -0,0 +1,15 @@ +import { eq } from 'drizzle-orm' +import { customAlphabet } from 'nanoid' +import slugify from 'slugify' +import { GenerateSlugArgs } from '../types' + +export const generateSlug = async (args: GenerateSlugArgs): Promise => { + const { value, connection, schema } = args + let slug = slugify(value.replace(/_/g, '-'), { lower: true, strict: true }) + const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 6) + while ((await connection.select().from(schema).where(eq(schema.slug, slug)).execute()).length > 0) { + slug = `${slug}-${nanoid()}` + } + + return slug +} diff --git a/apps/bc-wallet-api-server/src/utils/sort.ts b/apps/bc-wallet-api-server/src/utils/sort.ts new file mode 100644 index 000000000..c6b2c4cb7 --- /dev/null +++ b/apps/bc-wallet-api-server/src/utils/sort.ts @@ -0,0 +1,5 @@ +import { Step } from '../types' + +export const sortSteps = (steps: Step[]) => { + return steps?.sort((a, b) => a.order - b.order) +} diff --git a/apps/bc-wallet-api-server/tsconfig.json b/apps/bc-wallet-api-server/tsconfig.json new file mode 100644 index 000000000..4bd4ae9ce --- /dev/null +++ b/apps/bc-wallet-api-server/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationDir": "dist" + }, + "references": [ + { + "path": "../../packages/bc-wallet-openapi" + }, + { + "path": "../../packages/bc-wallet-adapter-client-api" + } + ], + "exclude": [ + "**/__tests__/**/*", + "**/dist/**/*", + "**/drizzle.config.ts" + ] +} diff --git a/apps/bc-wallet-demo-server/.dockerignore b/apps/bc-wallet-demo-server/.dockerignore new file mode 100644 index 000000000..8e36a023e --- /dev/null +++ b/apps/bc-wallet-demo-server/.dockerignore @@ -0,0 +1,23 @@ +**/node_modules +**/dist +**/build +**/coverage + +.git +.github +.gitignore + +**/.vscode +**/.idea +**/.DS_Store + +**/test +**/tests +**/__tests__ +**/cypress + +**/logs +**/*.log + +**/tmp +**/temp \ No newline at end of file diff --git a/apps/bc-wallet-demo-server/.env.example b/apps/bc-wallet-demo-server/.env.example new file mode 100644 index 000000000..c3386e64a --- /dev/null +++ b/apps/bc-wallet-demo-server/.env.example @@ -0,0 +1,13 @@ +TENANT_ID=000000000000000000000000 +API_KEY=000000000000000000000000 +WALLET_ID=000000000000000000000000 +BASE_ROUTE=/digital-trust/showcase +TRACTION_API_URL=https://traction-api.com +TRACTION_WEBHOOK_SECRET=secret + +# cors settings +CORS_DISABLED=false +CORS_ALLOW_ORIGINS=http://localhost:5003 +CORS_ALLOW_METHODS=GET,POST,PUT,DELETE,OPTIONS +CORS_ALLOW_HEADERS=Content-Type,Authorization,X-Requested-With,Cache-Control,Pragma,Expires +CORS_ALLOW_CREDENTIALS=true \ No newline at end of file diff --git a/apps/bc-wallet-demo-server/Dockerfile b/apps/bc-wallet-demo-server/Dockerfile new file mode 100644 index 000000000..742607443 --- /dev/null +++ b/apps/bc-wallet-demo-server/Dockerfile @@ -0,0 +1,46 @@ +# Build stage +FROM node:20-bookworm AS builder +SHELL ["/bin/bash", "-c"] +ENV SHELL=bash +ENV PNPM_HOME=/usr/local/share/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN npm -g install pnpm && pnpm setup && source /root/.bashrc && pnpm self-update && pnpm setup && pnpm config set store-dir "/usr/local/share/pnpm/store/v10" --global + +# Install dependencies +RUN apt-get update && \ + apt-get install -y build-essential python3 make g++ && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /build +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.json tsconfig.build.json tsconfig.eslint.json ./ +COPY packages packages +RUN rm -rf packages/bc-wallet-adapter-client-api # We are not generating the models for this one +COPY apps/tsconfig-base.json apps/ +COPY apps/bc-wallet-demo-server apps/bc-wallet-demo-server + +# Install dependencies and build +RUN rm -rf $(pnpm store path) && pnpm install -g typescript ts-node +RUN pnpm install --frozen-lockfile +RUN pnpm build:clean # FIXME turbo does not always build this correctly in docker compose for unknown reasons, so do a pre-build +RUN pnpm build +RUN pnpm deploy /deploy --filter bc-wallet-demo-server --prod --no-optional + + +# Runtime stage +FROM node:20-bookworm-slim + +ARG uid=1001 +ARG user=nodeapp +RUN useradd -U -ms /bin/bash -u $uid $user && usermod -a -G 0 $user +WORKDIR /app +COPY --from=builder /deploy /app +COPY apps/bc-wallet-demo-server/src/public /app/build/src/public +RUN chown -R $user:root /app && chmod -R ug+rw /app +ENV NODE_ENV=production +RUN echo PORT=3000 > .env + +USER $user +EXPOSE 3000 + +CMD ["node", "build/src/index.js"] \ No newline at end of file diff --git a/apps/bc-wallet-demo-server/config/businessCustom.js b/apps/bc-wallet-demo-server/config/businessCustom.js new file mode 100644 index 000000000..42b60ed66 --- /dev/null +++ b/apps/bc-wallet-demo-server/config/businessCustom.js @@ -0,0 +1,182 @@ +"use strict"; +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.businessCustom = void 0; +const dateint_1 = require("../src/utils/dateint"); +exports.businessCustom = { + name: 'Dave', + type: 'Proprietor', + image: '/public/student/student.svg', + hidden: true, + revocationInfo: [ + { + credentialName: 'Digital Business Card', + credentialIcon: '/public/student/icon-student.svg', + title: 'Revoke your Digital Business Card', + description: 'BC Registries allows you to revoke your Digital Business Card "if":\n• there is a problem with your credential.\n• your device was lost or stolen and you want to secure your personal information.', + }, + ], + progressBar: [ + { + name: 'person', + onboardingStep: 'PICK_CHARACTER', + iconLight: '/public/common/icon-person-light.svg', + iconDark: '/public/common/icon-person-dark.svg', + }, + { + name: 'moon', + onboardingStep: 'SETUP_START', + iconLight: '/public/common/icon-moon-light.svg', + iconDark: '/public/common/icon-moon-dark.svg', + }, + { + name: 'wallet', + onboardingStep: 'CHOOSE_WALLET', + iconLight: '/public/common/icon-wallet-light.svg', + iconDark: '/public/common/icon-wallet-dark.svg', + }, + { + name: 'notification', + onboardingStep: 'ACCEPT_CREDENTIAL', + iconLight: '/public/common/icon-notification-light.svg', + iconDark: '/public/common/icon-notification-dark.svg', + }, + { + name: 'balloon', + onboardingStep: 'SETUP_COMPLETED', + iconLight: '/public/common/icon-balloon-light.svg', + iconDark: '/public/common/icon-balloon-dark.svg', + }, + ], + onboarding: [ + { + screenId: 'PICK_CHARACTER', + title: 'Meet Dave', + text: "Meet Dave (that's you in this demo!). Dave is a sole proprietor. To help make his life easier, BC Registries is going to offer Dave a Digital Business Card to put in his BC Wallet.", + }, + { + screenId: 'SETUP_START', + title: "Let's get started!", + text: 'BC Wallet is a new app for storing and using credentials on your smartphone. Credentials are things like IDs, licenses and diplomas. \nUsing your BC Wallet is fast and simple. In the future it can be used online and in person. You approve every use, and share only what is needed. \nIn this demo, you will use your Digital Business Card to prove who you are and access services online instead of in-person.', + image: '/public/common/getStarted.svg', + }, + { + screenId: 'CHOOSE_WALLET', + title: 'Install BC Wallet', + text: 'First, install the BC Wallet app onto your smartphone. Select the button below for instructions and the next step.', + image: '/public/common/app-store-screenshots.png', + }, + { + screenId: 'CONNECT', + title: 'Connect with BC Registries', + text: 'Imagine, as Dave, you are logged into the BC Registries website (see below). They want to offer you a Digital Business Card. Use your BC Wallet to scan the QR code from the website.', + image: '/public/student/onboarding-connect-light.svg', + issuer_name: 'BC Registries', + }, + { + screenId: 'ACCEPT_CREDENTIAL', + title: 'Accept your digital business card', + text: "Your wallet now has a secure and private connection with BC Registries. You should have received an offer in BC Wallet for a Digital Business Card.\nReview what they are sending, and choose 'Accept offer'.", + image: '/public/common/onboarding-credential-light.svg', + credentials: [ + { + name: 'Digital Business Card', + version: (_a = process.env.BUSINESS_VERSION) !== null && _a !== void 0 ? _a : '1.0.0', + icon: '/public/student/icon-student.svg', + attributes: [ + { + name: 'business_name', + value: `DAVE'S CUPCAKES`, + }, + { + name: 'business_type', + value: 'BC Sole Proprietorship', + }, + { + name: 'company_status', + value: 'ACTIVE', + }, + { + name: 'cra_business_number', + value: '000000000000001', + }, + { + name: 'credential_id', + value: '00000001', + }, + { + name: 'family_name', + value: 'CHUNG', + }, + { + name: 'given_names', + value: 'DAVID, THOMAS', + }, + { + name: 'identifier', + value: 'FM000001', + }, + { + name: 'registered_on_dateint', + value: `${(0, dateint_1.getDateInt)(-1)}`, + }, + { + name: 'role', + value: 'PROPRIETOR', + }, + ], + }, + ], + }, + { + screenId: 'SETUP_COMPLETED', + title: "You're all set!", + text: 'Congratulations, you’ve just received your first digital credential. It is safely stored in your wallet and ready to be used. So, what are you waiting for? Let’s go!', + image: '/public/common/onboarding-completed-light.svg', + }, + ], + useCases: [ + { + id: 'cityOfVancouver', + name: 'City of Vancouver', + screens: [ + { + screenId: 'START', + title: 'Getting a ', + text: "Dave (that's you in this demo!) can get . In this example, you will just tell City of Vancouver you're the owner of your business.", + image: '/public/student/useCases/store/card-school.svg', + }, + { + screenId: 'CONNECTION', + title: "Start proving you're the owner of your business", + text: "Imagine, as Dave, you are in for City of Vancouver. They're offering you if you can prove you're the owner of your business. First, scan the QR code.", + image: '/public/student/useCases/store/cool-clothes-no-overlay.png', + verifier: { name: 'City of Vancouver', icon: '/public/student/useCases/store/logo-university.png' }, + }, + { + screenId: 'PROOF', + title: 'Confirm the information to send', + text: "BC Wallet will now ask you to confirm what to send. Notice how it will only share if the credential has expired, not even the expiry date itself gets shared. You don't have to share anything else for it to be trustable.", + requestOptions: { + title: 'City of Vancouver', + text: 'City of Vancouver would like some of your business information.', + requestedCredentials: [ + { + icon: '/public/student/useCases/school/icon-university-card.png', + name: 'Digital Business Card', + schema_id: 'K9igebFysBL6jcBwR8bKuN:2:Digital Business Card:1.0.0', + properties: ['business_name', 'business_type', 'company_status', 'family_name', 'given_names'], + }, + ], + }, + }, + { + screenId: 'STEP_END', + title: "You're done!", + text: "You proved that you're the owner of your business, and City of Vancouver . It only took a few seconds, you revealed minimal information, and City of Vancouver could easily and automatically trust what you sent.", + image: '/public/student/student-accepted.svg', + }, + ], + }, + ], +}; diff --git a/server/config/businessCustom.ts b/apps/bc-wallet-demo-server/config/businessCustom.ts similarity index 99% rename from server/config/businessCustom.ts rename to apps/bc-wallet-demo-server/config/businessCustom.ts index 21e1f18a2..f040e9915 100644 --- a/server/config/businessCustom.ts +++ b/apps/bc-wallet-demo-server/config/businessCustom.ts @@ -176,6 +176,6 @@ export const businessCustom: CustomCharacter = { image: '/public/student/student-accepted.svg', }, ], - } + }, ], } diff --git a/apps/bc-wallet-demo-server/config/lawyerCustom.js b/apps/bc-wallet-demo-server/config/lawyerCustom.js new file mode 100644 index 000000000..7f05e2382 --- /dev/null +++ b/apps/bc-wallet-demo-server/config/lawyerCustom.js @@ -0,0 +1,343 @@ +"use strict"; +var _a, _b; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.lawyerCustom = void 0; +const dateint_1 = require("../src/utils/dateint"); +const now = () => Math.floor(new Date().getTime() / 1000); +exports.lawyerCustom = { + name: 'Joyce', + type: 'Lawyer', + image: '/public/lawyer2/lawyer2.svg', + description: 'Joyce is a member of the Law Society of British Columbia looking to access court materials online.', + revocationInfo: [ + { + credentialName: 'Person', + credentialIcon: '/public/lawyer2/icon-lawyer2.svg', + title: 'Revoke your Person Credential', + description: 'Revoking your credentials does not mean that is it unusable. Services may accept revoked credentials especially for information that is unchanging, such as your birthday.', + }, + { + credentialName: 'member_card', + credentialIcon: '/public/lawyer2/icon-lawyer2.svg', + title: 'Revoke your LSBC Member Card', + description: 'The Law Society of BC allows you to revoke your own Member Card if:\n• there is a problem with your credential.\n• your device was lost or stolen and you want to secure your personal information.', + }, + ], + progressBar: [ + { + name: 'person', + onboardingStep: 'PICK_CHARACTER', + iconLight: '/public/common/icon-person-light.svg', + iconDark: '/public/common/icon-person-dark.svg', + }, + { + name: 'moon', + onboardingStep: 'SETUP_START', + iconLight: '/public/common/icon-moon-light.svg', + iconDark: '/public/common/icon-moon-dark.svg', + }, + { + name: 'wallet', + onboardingStep: 'CONNECT_LSBC', + iconLight: '/public/common/icon-wallet-light.svg', + iconDark: '/public/common/icon-wallet-dark.svg', + }, + { + name: 'notification', + onboardingStep: 'ACCEPT_PERSON', + iconLight: '/public/common/icon-notification-light.svg', + iconDark: '/public/common/icon-notification-dark.svg', + }, + { + name: 'balloon', + onboardingStep: 'SETUP_COMPLETED', + iconLight: '/public/common/icon-balloon-light.svg', + iconDark: '/public/common/icon-balloon-dark.svg', + }, + ], + onboarding: [ + { + screenId: 'PICK_CHARACTER', + title: 'Meet Joyce', + text: "Meet Joyce (that's you in this demo!). Joyce is a lawyer in good standing with The Law Society of British Columbia. We know this because she's got her physical Law Society of British Columbia Member Card. She's also a resident of British Columbia and has gone through the process to prove her identity with Service BC and has obtained a physical BC Services Card. This has allowed her to use in person services.", + }, + { + screenId: 'SETUP_START', + title: "Let's get started!", + text: 'BC Wallet is a new app for storing and using credentials on your smartphone. Credentials are things like IDs, licenses and diplomas. \nUsing your BC Wallet is fast and simple. In the future it can be used online and in person.You approve every use, and share only what is needed. \nIn this demo, you will use two credentials to prove who you are and access court materials online instead of in-person.', + image: '/public/lawyer2/onboarding/scan.svg', + }, + { + screenId: 'GOING_DIGITAL', + title: 'Going Digital', + text: 'Over time, The Government of B.C. and the Law Society of British Columbia has been making things more convenient by offering a choice of accessing services online and in-person. Joyce has accumulated all physical credentials and has gone digital. She has been verified as a practising lawyer in B.C. and verified herself with the BC Services Card app.', + image: '/public/lawyer2/onboarding/goingDigital.svg', + }, + { + screenId: 'ACCESS_COURT_MATERIALS', + title: 'Accessing court materials', + text: "Joyce has been going to the courthouse in person to access voice recordings and other confidential materials. Court Services Branch has let her know that she now has the choice of accessing court materials online. She just needs to prove she's a practising lawyer from B.C. and have a government issued ID with a matching name. She can get a digital lawyer member card from the Law Society of British Columbia and her Person credential from her BC Services Card app.", + image: '/public/lawyer2/onboarding/loginLSBC.svg', + }, + { + screenId: 'CONNECT_LSBC', + title: 'Get your lawyer credential', + text: 'Joyce is now ready to be issued her Law Society of British Columbia Member Card. She has logged into her member portal and is ready to accept a digital version of that card. Open the BC Wallet app on your phone, hit the scan button and accept.', + image: '/public/lawyer2/onboarding/lawyerCredPhone.svg', + issuer_name: 'Law Society of BC (Demo)', + }, + { + screenId: 'ACCEPT_LSBC', + title: 'Accept your lawyer credential', + text: 'Check your phone. You’ve received a credential offer from the Law Society of British Columbia in your BC Wallet. You can use this credential to prove you’re a lawyer online.', + image: '/public/lawyer2/onboarding/lawyerCredPhone.svg', + credentials: [ + { + name: 'member_card', + version: (_a = process.env.LAWYER_VERSION) !== null && _a !== void 0 ? _a : '1.0', + icon: '/public/lawyer2/connection/lsbc-logo.png', + attributes: [ + { + name: 'Member Status', + value: 'Active', + }, + { + name: 'Given Name', + value: 'Joyce', + }, + { + name: 'PPID', + value: 'MC12349', + }, + { + name: 'Member Status Code', + value: '1', + }, + { + name: 'Surname', + value: 'Lee-Martinez', + }, + ], + }, + ], + }, + { + screenId: 'CONNECT_PERSON', + title: 'Get Person credential', + text: 'Joyce gets her Person credential from the BC Services Card app. She starts the process within BC Wallet. For this demo you will scan this QR code to receive the credential offer.', + image: '/public/lawyer2/onboarding/personCredPhone.svg', + issuer_name: 'Service BC (Demo)', + }, + { + screenId: 'ACCEPT_PERSON', + title: 'Accept your Person credential', + text: 'Check your phone. You’ve received a credential offer from Service BC in your BC Wallet. You can use this credential to prove who you are online.', + image: '/public/lawyer2/onboarding/personCredPhone.svg', + credentials: [ + { + name: 'Person', + version: (_b = process.env.PERSON_VERSION) !== null && _b !== void 0 ? _b : '1.0', + icon: '/public/lawyer2/connection/bc-logo.png', + attributes: [ + { + name: 'postal_code', + value: 'V8N2X6', + }, + { + name: 'picture', + value: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAE0AAABcCAYAAAArgziOAAAABmJLR0QA/wD/AP+gvaeTAAAKqklEQVR42u2ceXAT5xXAlTad6TRN0yvTaf9okxbbEIKxtLa0ki/5knHwEd834AtjwuFgjDGH7WDMYQ7j2wLfhyQ7pRQMTNo0M7SlkzaTtGlpSJoMKRnI0YFSAgZsY/n1fWtpWR2OZdCxlvfNvJG0Wq20P717P0kkEkQQQQQRxEK8/P2f9BDL0z0pusFTTJ/xlNBv4+15vD3rRdHVXhL5MoqiviGQQvGk5AsRjA71HirMoFc9KdnWZ5TKb85LWN7e3k94SWRtCGLCBlgm6iGhL+BrvecVsEU+Mg88+X/MFpaZ3vISS33nR+zylj7rKZF/9ojAjHptkVjxM7cGtmRJwPfQtT60E7ApV6Xkr7q3lU3FMLC7+ij83BLYAoqW4AnqHQJNQte4Z2khoY86CBhgPfcXtwNGilI8uZsOgyahL7sdNAz+ixwIjOh1NgzIZN9xjwQgpmMdDO1dQwioIS2YAM021aJuM9zXu4W1eUqkgQ6FJpb1ch7rFy9Wfnvulxv4zePJjDkIGmn0JzmZ9IzbJAM8odcdAc1LQt948Fh+w0MiP433B/G+GsNCPU5RKnBbIbH2Z3yU351jGVQe7eC4ZovqEfI76M57FlLyJXPD2sT0b3kAjqs46JSliZKTv87fkRBF/Rjd5hLPwKHKPsAvNIrHPajiFzjS/hf/wE1NTBZKpT/g7dTWQ0y34Ae9z0N4ny6QyGjeWt3PqYCfotXtM0xxJ3gE7h4mjJS5kCSibLy44iydwCSRzfdZ200euuoEaQH5B0wc+DS66BU+JgaDjnj4KHz4xOwxvAj8Gx4DY0sSkrz4EccoOp+voOiAEPNtzfxwS5O+kT/6vEIJHzY1Q15iqmkL5urrrHhlqpaXwORKUMWmwKRWA7d6uiEwOJw7HDjnaisb4RuwxfJgkEXGQtXajQA6LaPDFa+Y7LNALA921cSjlG/AlviHgEwVi9Di4Pe1B1hoE2hxkeFR3KRwwlWztff5BMw7IJyBRXR5fBqMawZYaEQ7Skq5+98ngwenAiNzLN4Ao+TgE6xigRHtLN9hAozotY52wM/NtbaXnFxmyEr4AMzL1x+o0BdMgEViArjd22sBjWjS8jjOUi/Za852zWFXA3tOGgh+4TEmwIgOVeyyCozokXUbTLoEpVL5uDML2iuuLimkhoDP1fzsPNBj0J8O2rk9e82ugEmXOmkURD3l0vgVpLKARTQsJgmuHjs2LTBjXDMZWOL6YCdZGbO+1unAFvr5g2/ocqvA5MtehDcPHf5KYEaVc1srSl7urCQQ4IqCVaqKsQ4M9dSuGquASEdw4cgRuNzaym5LiY7nQKMPOal1ki9zWnZEXRoUYRUWUUVUPJyu3jOtVe1ZXTR1HHTr9pLNMDowAMWZqxir9fJVkOe6nDmdtT8gSoH1ViT4RcSA/IUECIxLhZCkbAhNXgmhKajJK0CZlAXB8RkQEJMC4VhavHW4blpgN7Hs6KmshtycIlDh6yPS8yAiI99Ew9Jy7+PtWxEZhR4OhiYPs2+tpQAqLBpCU3NMTojAIoAC49IYgIFx6RAUn8lsV2XmQ1RmAezctBX+XN8IE5oHGfNiaxvs3rydeT46ezVUlpTD4O598OuaffBBqxqqitaDJCQKj5XBvld4ZkGBY3tOipbZJbAjLF+0qrC0HAsLIICsuWNCUgac238QrnV3Q115Bbt/0qoiKNmwGVbkr2e3Hcbnb/f1WVhgp6GdIi6L732P7KtKK1A49mIxLl+fCUhcVDT0bdkK5/fvh1OVVVC+Mg+el/ozzy3yCwApZjt0DRNQIeh+oSlTAOmoBBNYSclZcLyqGsYG+k0A1JbttABOdPP6UiYJWHNbTdk29nMGJ2Z9jvs7vjMgVTS+4fh0wMpX5aK7DFh82H+3tEyq4pJvhaebwgqKT8cYFs/A8cdYRbaRmEYj2KKVBYxl6aeJW2f3H2L2j8kuhK6qGigoLGYen9hby0A7ubMSzlbtMgF4YkcF+1lDU1Z9oUrPe9FZbdSb1oAtw/HLWH//tMH5QnPLxxEZJCDngX90MviGLWdHOURpzIbkpGtLyuBTs0L1UksLlGStYqYVxm1DGKsYSBivJnp64L2WNubxGYT56rYd7Oc6jdZufM2pikrWPSPS80cjk/O+79KJ7WuvVM9YXG7KX/uRd2AYSDGeRaOVrUFrOrBxM1NrfaJWQ+yKQnj9oGWhmpuYwr7Pew0NrHtGo5WN4HR2pLGRsaj4lWvQ6nZD4/pidn9SbrCgDTCfkwWRL+8T501tJXSMOTAKC9Dxgf4ZoY3iPnetBGijknhEgrj59tLsnKlBI9ZY/2lvZwBl5a2DmtLtoO/vg1t1dTCGbdK24jLmGF929zCvKVuRw4y8jcfp2VLGHAet/Hp4Rn6/E5ciUE9h4XmHCy0jNtGmNmYmPb6nFq1tDVNncbff7euFwfLtcBFLDPKYlBrEFd9pbAY9Pkeg3a6vh1/t3s+UG9e7u60eX/3yJsY1MZ7dRGgJTh53yzq40IpSM+0CbQStMBFLiL2lO6adWHze2QmpOWsZi2LG2QZoRK81NTEuSkqQESsW3bhhIyacZJK9ryqVVY87FRr5LRMXWlpMgl2gET1/pAGWZRVA2cYt8Fe8DHcDgzypua6gW2qr9zJQ0nNfgi86uyygEf3TgcNMYUv2IYUtKXg/PtoObxysg2SMmaGpufdDUnOXilwh5Kc57IxeGsDEK3uBe7uxCQrXFFvUYCRRHNpaAf9FkOyFEzNoJCl81KZm4puK89oktM6clYWw1D8s1XXXPX1pf+6qbFID2QuaUUls+ntTKxO7iLWMD1jWgObQmKRgKFnuYpK4jPdJF0HcfWdO/iWylMLVy6u62WIxNNIkUzlLJ3ofJIK7OAYa7+gA/TRW3/7ylrUuX5rwrEz2I+7ShPykNLjX3+dUaJPYgUz09ti2v1b7E56sTZOpuMtIE/Gqz/uGAtR8KPi/ri6nWyJHb0NV1dd4s94Kh5PF3Gy6CGuhdWlZ8MvtO+CPe/eBrnwbpMbEM0sEXAjtD/xbPopLmWaagNgLGolZJAHM6nWD2sO8XEaKSzXLnAHtPiYcY3kx1n4M45rGhtfpEkR8lanfMVlf6W0398T4eKe5mQU3aWUcZaajcPLkkyI+ywKxQo6QLk4HjTTZNpzojOD0thfUw6K5IOR37wZ3HbNmaffa2mZz0o+mgwORorkkzD/GkH+6wvUTXGjE2khBSkY6oNU4EJrubwDwmGguCvnN0oW6+ndNq/keBtwI1nXjnR2OgDcJQ5oA0VwWrMg1FuUDWtydpka2FSJuS7KjfQDqKkRzXaxBM+p4VyeTCY1NNwNQ3YYW2PkwsU8POs3uOeuWtkJjazCcSBjLCa7eaWwcg6EBKVrPajxOK+77O9RLqOMmrZJOdwafd5+/GLMFGnfkM3pUzVgcU481NIx+5bG7utzz3/9mA83U+rrQVdVXRfNRHhaaoXd8Q4A2ex0WoM26bdIOCtBmr13zE5pO2/4IMa1+nkLT+U0Vng/lnkmi+SpoMZUPAe2foFbP37+wJq0NWtx2my1uUPsZDA15iQRBeBqNN0I5jnp3GmBfoqpxvx8KtMzhDQ9/CwYHg2BQk44WWMTcarWBbtsWCSKIIIIIIogggggiCJ/k//d9/eRF/vu8AAAAAElFTkSuQmCC', + }, + { + name: 'given_names', + value: 'Joyce', + }, + { + name: 'family_name', + value: 'Lee-Martinez', + }, + { + name: 'locality', + value: 'BC', + }, + { + name: 'region', + value: 'Victoria', + }, + { + name: 'street_address', + value: '123 Test Rd', + }, + { + name: 'country', + value: 'Canada', + }, + { + name: 'expiry_date_dateint', + value: `${(0, dateint_1.getDateInt)(5)}`, + }, + { + name: 'birthdate_dateint', + value: `${(0, dateint_1.getDateInt)(-19)}`, + }, + ], + }, + ], + }, + { + screenId: 'SETUP_COMPLETED', + title: "You're all set!", + text: 'Congratulations, you’ve just received your first digital credentials. They are safely stored in your wallet and ready to be used. So, what are you waiting for? Let’s go!', + image: '/public/lawyer2/onboarding/lawyer2Success.svg', + }, + ], + useCases: [ + { + id: 'courtServices', + name: 'Court Services', + screens: [ + { + screenId: 'START', + title: 'Gain access to court materials online', + text: 'Joyce can gain entry to Access to Court Materials, a service online by Court Services Branch where lawyers can access court documents online. They require proof that you’re a practising lawyer in B.C. and a matching name to your lawyer member card.', + image: '/public/lawyer2/useCases/courtServices/bothCreds.svg', + }, + { + screenId: 'CONNECTION', + title: 'Start proving you’re a lawyer', + text: 'As Joyce you’re now ready to prove you’re a practising lawyer in B.C. and your name to Court Services Branch and gain entry to Access to Court Materials online. Scan the QR code.', + image: '/public/lawyer2/useCases/courtServices/courtServicesOverlay.png', + verifier: { name: 'Court Services Branch (DEMO)', icon: '/public/lawyer2/connection/lsbc-logo.png' }, + }, + { + screenId: 'PROOF', + title: 'Confirm the information to send', + text: 'BC Wallet will now ask you to confirm what to send. Notice how you’re not sharing your entire credential. Court Services Branch is requesting that you prove only what is needed.', + requestOptions: { + title: 'Court Services Branch (DEMO) Request', + text: 'Court Services Branch (DEMO) would like some of your personal information.', + requestedCredentials: [ + { + icon: '/public/lawyer2/connection/lsbc-logo.png', + name: 'member_card', + schema_id: 'QEquAHkM35w4XVT3Ku5yat:2:member_card:1.54', + properties: ['Given Name', 'Surname', 'PPID'], + }, + { + icon: '/public/lawyer2/connection/bc-logo.png', + name: 'Person', + schema_id: 'QEquAHkM35w4XVT3Ku5yat:2:Person:1.3', + properties: ['given_names', 'family_name'], + }, + ], + }, + }, + { + screenId: 'STEP_END', + title: "You're done!", + text: 'You’ve proved to Court Services Branch that you’re a practising lawyer from B.C. and your identity using your Person credential. You can now access court materials online from the comfort of your own home. It only took a few seconds and you revealed minimal information that Court Services Branch could easily and automatically trust.', + image: '/public/lawyer2/onboarding/lawyer2Success.svg', + }, + ], + }, + { + id: 'courtLibrary', + name: 'Court Library', + hidden: true, + screens: [ + { + screenId: 'START', + title: 'Gain access to the Court Library in person', + text: 'Joyce wants to gain access to the court library using her digital credentials. The court library requires that joyce provides photo identification.', + image: '/public/lawyer2/useCases/courtServices/bothCreds.svg', + }, + { + screenId: 'CONNECTION', + title: 'Start proving you’re a lawyer', + text: 'As Joyce you’re now ready to prove you’re a practising lawyer in B.C. to the court library. Scan the QR code.', + image: '/public/lawyer2/useCases/courtServices/courtServicesOverlay.png', + verifier: { name: 'Court Library', icon: '/public/lawyer2/connection/lsbc-logo.png' }, + }, + { + screenId: 'PROOF', + title: 'Confirm the information to send', + text: 'BC Wallet will now ask you to confirm what to send. Notice how you’re not sharing your entire credential. The Court Library is requesting that you prove only what is needed.', + requestOptions: { + title: 'Court Library (DEMO) Request', + text: 'Court Library (DEMO) would like some of your personal information.', + requestedCredentials: [ + { + icon: '/public/lawyer2/connection/lsbc-logo.png', + name: 'member_card', + schema_id: 'QEquAHkM35w4XVT3Ku5yat:2:member_card:1.54', + properties: ['Given Name', 'Surname', 'PPID'], + nonRevoked: { to: now() }, + }, + { + icon: '/public/lawyer2/connection/bc-logo.png', + name: 'Person', + schema_id: 'QEquAHkM35w4XVT3Ku5yat:2:Person:1.3', + properties: ['given_names', 'family_name', 'picture'], + nonRevoked: { to: now() }, + }, + ], + }, + }, + { + screenId: 'STEP_END', + title: "You're done!", + text: 'You’ve proved to the Court Library that you’re a practising lawyer from B.C. and your identity using your Person credential. You can now access court materials online from the comfort of your own home. It only took a few seconds and you revealed minimal information that Court Services Branch could easily and automatically trust.', + image: '/public/lawyer2/onboarding/lawyer2Success.svg', + }, + ], + }, + { + id: 'attestation', + name: 'BC Wallet Attestation', + hidden: true, + screens: [ + { + screenId: 'START', + title: 'Verify a BC Wallet application', + text: 'Verify that you are using a valid BC Wallet application.', + image: '/public/lawyer2/useCases/courtServices/bothCreds.svg', + }, + { + screenId: 'CONNECTION', + title: 'Connect to Attestation Service', + text: "You're now ready to prove you're using a valid BC Wallet application. Scan the QR code.", + image: '/public/lawyer2/useCases/courtServices/courtServicesOverlay.png', + verifier: { name: 'BC Wallet Attestation', icon: '/public/lawyer2/connection/bc-logo.png' }, + }, + { + screenId: 'PROOF', + title: 'Confirm the information to send', + text: 'BC Wallet will now ask you to confirm what to send. Notice how you’re not sharing your entire credential.', + requestOptions: { + title: 'Attestation (DEMO) Request', + text: 'Attestation (DEMO) would like some of your personal information.', + requestedCredentials: [ + { + icon: '/public/lawyer2/connection/bc-logo.png', + name: 'Certified Application', + properties: ['Issued At', 'Assurance Level'], + nonRevoked: { to: now() }, + }, + { + icon: '/public/lawyer2/connection/bc-logo.png', + name: 'Person', + properties: ['given_names', 'family_name', 'picture'], + nonRevoked: { to: now() }, + }, + ], + }, + }, + { + screenId: 'STEP_END', + title: "You're done!", + text: 'You’ve proved to the Attestation Service that you’re using a valid BC Wallet application. It only took a few seconds and you revealed minimal information that Attestation Service could easily and automatically trust.', + image: '/public/lawyer2/onboarding/lawyer2Success.svg', + }, + ], + }, + ], +}; diff --git a/server/config/lawyerCustom.ts b/apps/bc-wallet-demo-server/config/lawyerCustom.ts similarity index 99% rename from server/config/lawyerCustom.ts rename to apps/bc-wallet-demo-server/config/lawyerCustom.ts index 42611ae1c..5dcd2dcf8 100644 --- a/server/config/lawyerCustom.ts +++ b/apps/bc-wallet-demo-server/config/lawyerCustom.ts @@ -1,5 +1,4 @@ import type { CustomCharacter } from '../src/content/types' - import { getDateInt } from '../src/utils/dateint' const now = () => Math.floor(new Date().getTime() / 1000) diff --git a/server/config/nginx.conf b/apps/bc-wallet-demo-server/config/nginx.conf similarity index 100% rename from server/config/nginx.conf rename to apps/bc-wallet-demo-server/config/nginx.conf diff --git a/apps/bc-wallet-demo-server/config/studentCustom.js b/apps/bc-wallet-demo-server/config/studentCustom.js new file mode 100644 index 000000000..18bb6d191 --- /dev/null +++ b/apps/bc-wallet-demo-server/config/studentCustom.js @@ -0,0 +1,199 @@ +"use strict"; +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.studentCustom = void 0; +const dateint_1 = require("../src/utils/dateint"); +exports.studentCustom = { + name: 'Alice', + type: 'Student', + image: '/public/student/student.svg', + revocationInfo: [ + { + credentialName: 'Student Card', + credentialIcon: '/public/student/icon-student.svg', + title: 'Revoke your Student Card', + description: 'Best BC College allows you to revoke your Student Card "if":\n• there is a problem with your credential.\n• your device was lost or stolen and you want to secure your personal information.', + }, + ], + progressBar: [ + { + name: 'person', + onboardingStep: 'PICK_CHARACTER', + iconLight: '/public/common/icon-person-light.svg', + iconDark: '/public/common/icon-person-dark.svg', + }, + { + name: 'moon', + onboardingStep: 'SETUP_START', + iconLight: '/public/common/icon-moon-light.svg', + iconDark: '/public/common/icon-moon-dark.svg', + }, + { + name: 'wallet', + onboardingStep: 'CHOOSE_WALLET', + iconLight: '/public/common/icon-wallet-light.svg', + iconDark: '/public/common/icon-wallet-dark.svg', + }, + { + name: 'notification', + onboardingStep: 'ACCEPT_CREDENTIAL', + iconLight: '/public/common/icon-notification-light.svg', + iconDark: '/public/common/icon-notification-dark.svg', + }, + { + name: 'balloon', + onboardingStep: 'SETUP_COMPLETED', + iconLight: '/public/common/icon-balloon-light.svg', + iconDark: '/public/common/icon-balloon-dark.svg', + }, + ], + onboarding: [ + { + screenId: 'PICK_CHARACTER', + title: 'Meet Alice', + text: "Meet Alice (that's you in this demo!). Alice is a student at BestBC College. To help make student life easier, BestBC College is going to offer Alice a digital Student Card to put in her BC Wallet.", + }, + { + screenId: 'SETUP_START', + title: "Let's get started!", + text: 'BC Wallet is a new app for storing and using credentials on your smartphone. Credentials are things like IDs, licenses and diplomas. \nUsing your BC Wallet is fast and simple. In the future it can be used online and in person. You approve every use, and share only what is needed. \nIn this demo, you will use two credentials to prove who you are and access court materials online instead of in-person.', + image: '/public/common/getStarted.svg', + }, + { + screenId: 'CHOOSE_WALLET', + title: 'Install BC Wallet', + text: 'First, install the BC Wallet app onto your smartphone. Select the button below for instructions and the next step.', + image: '/public/common/app-store-screenshots.png', + }, + { + screenId: 'CONNECT', + title: 'Connect with BestBC College', + text: 'Imagine, as Alice, you are logged into the BestBC College website (see below). They want to offer you a Digital Student Card. Use your BC Wallet to scan the QR code from the website.', + image: '/public/student/onboarding-connect-light.svg', + issuer_name: 'BestBC College', + }, + { + screenId: 'ACCEPT_CREDENTIAL', + title: 'Accept your student card', + text: "Your wallet now has a secure and private connection with BestBC College. You should have received an offer in BC Wallet for a Student Card.\nReview what they are sending, and choose 'Accept offer'.", + image: '/public/common/onboarding-credential-light.svg', + credentials: [ + { + name: 'student_card', + version: (_a = process.env.STUDENT_VERSION) !== null && _a !== void 0 ? _a : '1.0', + icon: '/public/student/icon-student.svg', + attributes: [ + { + name: 'student_first_name', + value: 'Alice', + }, + { + name: 'student_last_name', + value: 'Smith', + }, + { + name: 'expiry_date', + value: `${(0, dateint_1.getDateInt)(4)}`, + }, + ], + }, + ], + }, + { + screenId: 'SETUP_COMPLETED', + title: "You're all set!", + text: 'Congratulations, you’ve just received your first digital credentials. They are safely stored in your wallet and ready to be used. So, what are you waiting for? Let’s go!', + image: '/public/common/onboarding-completed-light.svg', + }, + ], + useCases: [ + { + id: 'clothesOnline', + name: 'Cool Clothes Online', + screens: [ + { + screenId: 'START', + title: 'Getting a student discount', + text: "Alice (that's you in this demo!) can get a student discount on her online purchase. In this example, you will just tell Cool Clothes Online you're a student.", + image: '/public/student/useCases/store/card-school.svg', + }, + { + screenId: 'CONNECTION', + title: "Start proving you're a student", + text: "Imagine, as Alice, you are in the checkout process for Cool Clothes Online. They're offering you a 15% discount on your purchase if you can prove you're a student. First, scan the QR code.", + image: '/public/student/useCases/store/cool-clothes-no-overlay.png', + verifier: { name: 'Cool Clothes Online', icon: '/public/student/useCases/store/logo-university.png' }, + }, + { + screenId: 'PROOF', + title: 'Confirm the information to send', + text: "BC Wallet will now ask you to confirm what to send. Notice how it will only share if the credential has expired, not even the expiry date itself gets shared. You don't have to share anything else for it to be trustable.", + requestOptions: { + title: 'Cool Clothes Online Request', + text: 'Cool Clothes Online would like some of your personal information.', + requestedCredentials: [ + { + icon: '/public/student/useCases/school/icon-university-card.png', + name: 'student_card', + schema_id: 'QEquAHkM35w4XVT3Ku5yat:2:student_card:1.6', + predicates: { + name: 'expiry_date', + type: '>=', + value: (0, dateint_1.getDateInt)(), + }, + }, + ], + }, + }, + { + screenId: 'STEP_END', + title: "You're done!", + text: "You proved that you're a student, and Cool Clothes Online gave you the discount. It only took a few seconds, you revealed minimal information, and Cool Clothes Online could easily and automatically trust what you sent.", + image: '/public/student/student-accepted.svg', + }, + ], + }, + { + id: 'study', + name: 'BestBC College', + screens: [ + { + screenId: 'START', + title: 'Book a study room', + text: "Alice has lots of work to do, and needs a study room for some peace and quiet. In this example, we'll present some info from our Student Card, but just what's needed to book the room.", + image: '/public/student/useCases/school/card-school.svg', + }, + { + screenId: 'CONNECTION', + title: 'Start booking the room', + text: "Imagine you're on the room booking page for BestBC College, abd you've chosen a data and time. Now they just need to confirm a few details. Scan the QR code to continue.", + image: '/public/student/useCases/school/best-bc-college-no-overlay.png', + verifier: { name: 'BestBC College', icon: '/public/student/useCases/school/logo-university.png' }, + }, + { + screenId: 'PROOF', + title: 'Confirm the information to send', + text: "BC Wallet will now ask you to confirm what to send for the booking. Notice how they only need your first name so they can display it on the booking screen. By providing anything from your student card, they automatically know your student card hasn't been revoked.", + requestOptions: { + title: 'BestBC College Request', + text: 'BestBC College would like some of your personal information.', + requestedCredentials: [ + { + icon: '/public/student/useCases/school/icon-university-card.png', + name: 'student_card', + schema_id: 'QEquAHkM35w4XVT3Ku5yat:2:student_card:1.6', + properties: ['student_first_name'], + }, + ], + }, + }, + { + screenId: 'STEP_END', + title: "You're done!", + text: "The room is booked. Just by proving your first name, Best BC College could trust you are a current student, and could let others know there's a booking without revealing too much about you.", + image: '/public/student/student-accepted.svg', + }, + ], + }, + ], +}; diff --git a/server/config/studentCustom.ts b/apps/bc-wallet-demo-server/config/studentCustom.ts similarity index 99% rename from server/config/studentCustom.ts rename to apps/bc-wallet-demo-server/config/studentCustom.ts index 92ed98d2f..0cdc5ea01 100644 --- a/server/config/studentCustom.ts +++ b/apps/bc-wallet-demo-server/config/studentCustom.ts @@ -1,5 +1,4 @@ import type { CustomCharacter } from '../src/content/types' - import { getDateInt } from '../src/utils/dateint' export const studentCustom: CustomCharacter = { name: 'Alice', diff --git a/server/package.json b/apps/bc-wallet-demo-server/package.json similarity index 69% rename from server/package.json rename to apps/bc-wallet-demo-server/package.json index efc627378..ab5a0346f 100644 --- a/server/package.json +++ b/apps/bc-wallet-demo-server/package.json @@ -1,5 +1,5 @@ { - "name": "server", + "name": "bc-wallet-demo-server", "version": "1.0.0", "private": true, "description": "", @@ -7,7 +7,8 @@ "scripts": { "start": "ts-node -r dotenv/config src/index.ts dotenv_config_path=.env", "dev": "tsnd -r dotenv/config --respawn src/index.ts dotenv_config_path=.env", - "build": "rm -rf build && tsc && cp -r src/public build", + "build": "rimraf build && tsc && copyfiles -u 1 src/public/**/* build", + "build:clean": "tsc --build --clean && tsc --build", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -15,16 +16,17 @@ "license": "ISC", "dependencies": { "@types/cron": "^1.7.3", - "@types/express": "^4.17.13", + "@types/express": "^5.0.0", "@types/lodash": "^4.14.194", "@types/uuid": "^8.3.3", - "axios": "^0.27.2", - "class-transformer": "^0.3.1", + "axios": "^1.8.1", + "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cors": "^2.8.5", "cron": "^1.8.2", "express": "^4.17.1", "moment": "^2.29.4", + "reflect-metadata": "^0.2.2", "routing-controllers": "^0.9.0", "socket.io": "^4.7.5", "ts-node": "^10.9.2", @@ -33,8 +35,9 @@ "ws": "^8.18.0" }, "devDependencies": { - "dotenv": "^16.0.0", + "@types/cors": "^2.8.17", + "dotenv": "^16.4.7", "ts-node-dev": "^2.0.0", - "typescript": "^4.5.2" + "typescript": "^5" } } diff --git a/server/src/content/Characters.ts b/apps/bc-wallet-demo-server/src/content/Characters.ts similarity index 99% rename from server/src/content/Characters.ts rename to apps/bc-wallet-demo-server/src/content/Characters.ts index ae7d72526..969b06249 100644 --- a/server/src/content/Characters.ts +++ b/apps/bc-wallet-demo-server/src/content/Characters.ts @@ -1,8 +1,7 @@ -import type { CustomCharacter } from './types' - +import { businessCustom } from '../../config/businessCustom' import { lawyerCustom } from '../../config/lawyerCustom' import { studentCustom } from '../../config/studentCustom' -import { businessCustom } from '../../config/businessCustom' +import type { CustomCharacter } from './types' const characters: CustomCharacter[] = [studentCustom, lawyerCustom, businessCustom] diff --git a/server/src/content/misc/Wallets.ts b/apps/bc-wallet-demo-server/src/content/misc/Wallets.ts similarity index 100% rename from server/src/content/misc/Wallets.ts rename to apps/bc-wallet-demo-server/src/content/misc/Wallets.ts diff --git a/server/src/content/types.ts b/apps/bc-wallet-demo-server/src/content/types.ts similarity index 100% rename from server/src/content/types.ts rename to apps/bc-wallet-demo-server/src/content/types.ts diff --git a/server/src/controllers/ConnectionController.ts b/apps/bc-wallet-demo-server/src/controllers/ConnectionController.ts similarity index 99% rename from server/src/controllers/ConnectionController.ts rename to apps/bc-wallet-demo-server/src/controllers/ConnectionController.ts index aa25f80db..ba02ed11f 100644 --- a/server/src/controllers/ConnectionController.ts +++ b/apps/bc-wallet-demo-server/src/controllers/ConnectionController.ts @@ -24,7 +24,6 @@ export class ConnectionController { ...params, accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], alias: 'connection', - goal: '', handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], protocol_version: '1.1', use_public_did: false, diff --git a/server/src/controllers/CredentialController.ts b/apps/bc-wallet-demo-server/src/controllers/CredentialController.ts similarity index 90% rename from server/src/controllers/CredentialController.ts rename to apps/bc-wallet-demo-server/src/controllers/CredentialController.ts index b5e4d6fe4..e3fe572a0 100644 --- a/server/src/controllers/CredentialController.ts +++ b/apps/bc-wallet-demo-server/src/controllers/CredentialController.ts @@ -1,8 +1,9 @@ -import { Body, Delete, Get, InternalServerError, JsonController, NotFoundError, Param, Post } from 'routing-controllers' -import { Inject, Service } from 'typedi' +import { Body, Get, JsonController, Param, Post } from 'routing-controllers' +import { Service } from 'typedi' -import { Credential } from '../content/types' +import type { Credential } from '../content/types' import { tractionRequest } from '../utils/tractionHelper' + @JsonController('/credentials') @Service() export class CredentialController { diff --git a/server/src/controllers/DeepLinkController.ts b/apps/bc-wallet-demo-server/src/controllers/DeepLinkController.ts similarity index 100% rename from server/src/controllers/DeepLinkController.ts rename to apps/bc-wallet-demo-server/src/controllers/DeepLinkController.ts diff --git a/server/src/controllers/ProofController.ts b/apps/bc-wallet-demo-server/src/controllers/ProofController.ts similarity index 96% rename from server/src/controllers/ProofController.ts rename to apps/bc-wallet-demo-server/src/controllers/ProofController.ts index 5b759e9cd..cd61156b5 100644 --- a/server/src/controllers/ProofController.ts +++ b/apps/bc-wallet-demo-server/src/controllers/ProofController.ts @@ -1,7 +1,7 @@ import { Body, Delete, Get, JsonController, Param, Post } from 'routing-controllers' import { Service } from 'typedi' -import { tractionBaseUrl, tractionRequest } from '../utils/tractionHelper' +import { tractionRequest } from '../utils/tractionHelper' @JsonController('/proofs') @Service() diff --git a/server/src/controllers/RevocationController.ts b/apps/bc-wallet-demo-server/src/controllers/RevocationController.ts similarity index 100% rename from server/src/controllers/RevocationController.ts rename to apps/bc-wallet-demo-server/src/controllers/RevocationController.ts diff --git a/server/src/controllers/UseCaseController.ts b/apps/bc-wallet-demo-server/src/controllers/UseCaseController.ts similarity index 96% rename from server/src/controllers/UseCaseController.ts rename to apps/bc-wallet-demo-server/src/controllers/UseCaseController.ts index c300408f3..f0f6268ec 100644 --- a/server/src/controllers/UseCaseController.ts +++ b/apps/bc-wallet-demo-server/src/controllers/UseCaseController.ts @@ -1,5 +1,5 @@ import { Get, JsonController, NotFoundError, Param, QueryParam } from 'routing-controllers' -import { Inject, Service } from 'typedi' +import { Service } from 'typedi' import characters from '../content/Characters' diff --git a/server/src/controllers/WalletController.ts b/apps/bc-wallet-demo-server/src/controllers/WalletController.ts similarity index 99% rename from server/src/controllers/WalletController.ts rename to apps/bc-wallet-demo-server/src/controllers/WalletController.ts index a28467328..0ef45218e 100644 --- a/server/src/controllers/WalletController.ts +++ b/apps/bc-wallet-demo-server/src/controllers/WalletController.ts @@ -1,9 +1,8 @@ -import type { Wallet } from '../content/types' - import { Get, JsonController } from 'routing-controllers' import { Service } from 'typedi' import { Wallets } from '../content/misc/Wallets' +import type { Wallet } from '../content/types' @JsonController('/wallets') @Service() diff --git a/server/src/controllers/WebhookController.ts b/apps/bc-wallet-demo-server/src/controllers/WebhookController.ts similarity index 93% rename from server/src/controllers/WebhookController.ts rename to apps/bc-wallet-demo-server/src/controllers/WebhookController.ts index c0b805aac..479b23d2c 100644 --- a/server/src/controllers/WebhookController.ts +++ b/apps/bc-wallet-demo-server/src/controllers/WebhookController.ts @@ -1,6 +1,5 @@ -import type { Socket } from 'socket.io' - import { Body, JsonController, Post, Req } from 'routing-controllers' +import type { Socket } from 'socket.io' import { Service } from 'typedi' @JsonController('/whook/topic') @@ -10,7 +9,7 @@ export class WebhookController { public async handlePostWhook(@Body() params: any, @Req() req: any) { const socketMap: Map = req.app.get('sockets') const api_key = req.headers['x-api-key'] - if (api_key !== process.env.WEBHOOK_SECRET) { + if (api_key !== process.env.TRACTION_WEBHOOK_SECRET) { return { message: 'Unauthorized', status: 401 } } const connectionId = params.connection_id diff --git a/apps/bc-wallet-demo-server/src/index.ts b/apps/bc-wallet-demo-server/src/index.ts new file mode 100644 index 000000000..41ccfe09c --- /dev/null +++ b/apps/bc-wallet-demo-server/src/index.ts @@ -0,0 +1,119 @@ +import 'reflect-metadata' + +import type { Express } from 'express' +import { json, static as stx } from 'express' +import * as http from 'http' +import { createExpressServer } from 'routing-controllers' +import { Server } from 'socket.io' + +import { corsOptions } from './utils/cors' +import { tractionApiKeyUpdaterInit, tractionGarbageCollection, tractionRequest } from './utils/tractionHelper' + +const baseRoute = process.env.BASE_ROUTE +console.log(`Starting server with BASE_ROUTE: ${baseRoute}`) +console.log( + `Environment variables loaded: ${JSON.stringify({ + PORT: process.env.PORT, + BASE_ROUTE: process.env.BASE_ROUTE, + CORS_DISABLED: process.env.CORS_DISABLED, + })}`, +) + +const app: Express = createExpressServer({ + controllers: [__dirname + '/controllers/**/*{.js,.ts}'], + cors: corsOptions, + routePrefix: `${baseRoute}/demo`, +}) + +const server = http.createServer(app) + +const ws = new Server(server, { + cors: corsOptions, + path: `${baseRoute}/socket/`, +}) + +const socketMap = new Map() +const connectionMap = new Map() + +ws.on('connection', (socket) => { + socket.on('subscribe', ({ connectionId }) => { + if (connectionId) { + socketMap.set(connectionId, socket) + connectionMap.set(socket.id, connectionId) + } + }) + socket.on('disconnect', () => { + const connectionId = connectionMap.get(socket.id) + connectionMap.delete(socket.id) + if (connectionId) { + socketMap.delete(connectionId) + } + }) +}) + +const run = async () => { + try { + console.debug('Initializing Traction API key updater') + await tractionApiKeyUpdaterInit() + console.debug('Running Traction garbage collection') + await tractionGarbageCollection() + + app.set('sockets', socketMap) + + app.use(json()) + console.log(`Setting up static route: ${baseRoute}/public -> ${__dirname}/public`) + app.use(`${baseRoute}/public`, stx(__dirname + '/public')) + + app.get(`${baseRoute}/server/last-reset`, (req, res) => { + console.debug(`${req.method} ${req.path} - handling last-reset request`) + res.send(new Date()) + }) + + // Redirect QR code scans for installing bc wallet to the apple or google play store + const androidUrl = 'https://play.google.com/store/apps/details?id=ca.bc.gov.BCWallet' + const appleUrl = 'https://apps.apple.com/us/app/bc-wallet/id1587380443' + app.get(`${baseRoute}/qr`, (req, res) => { + console.debug(`${req.method} ${req.path} - handling QR redirect`) + const appleMatchers = [/iPhone/i, /iPad/i, /iPod/i] + let url = androidUrl + const isApple = appleMatchers.some((item) => req.get('User-Agent')?.match(item)) + if (isApple) { + url = appleUrl + } + res.redirect(url) + }) + + // respond to healthchecks for openshift + app.get('/', (_, res) => { + res.send('ok') + }) + + // respond to ditp health checks + app.get(`${baseRoute}/server/ready`, (_, res) => { + res.json({ ready: true }) + }) + + // respond to ready checks to the traction agent + app.get(`${baseRoute}/agent/ready`, async (_, res) => { + try { + const response = await tractionRequest.get(`/status/ready`) + res.send(response.data) + } catch (error) { + console.error('Error in agent ready check:', error) + return Promise.reject(Error('Failed to check agent readiness')) + } + }) + + const port = process.env.PORT || 3000 + server.listen(port, () => { + console.log(`Server started on port ${port}`) + console.log(`Full base URL: http://localhost:${port}${baseRoute}`) + }) + } catch (error) { + console.error('Error during server startup:', error) + process.exit(1) + } +} + +console.log('Starting application...') +void run() diff --git a/server/src/public/animo-logo.png b/apps/bc-wallet-demo-server/src/public/animo-logo.png similarity index 100% rename from server/src/public/animo-logo.png rename to apps/bc-wallet-demo-server/src/public/animo-logo.png diff --git a/server/src/public/businesswoman/businesswoman.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/businesswoman.svg similarity index 100% rename from server/src/public/businesswoman/businesswoman.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/businesswoman.svg diff --git a/server/src/public/businesswoman/icon-businesswoman.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/icon-businesswoman.svg similarity index 100% rename from server/src/public/businesswoman/icon-businesswoman.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/icon-businesswoman.svg diff --git a/server/src/public/businesswoman/icon-creditcard.png b/apps/bc-wallet-demo-server/src/public/businesswoman/icon-creditcard.png similarity index 100% rename from server/src/public/businesswoman/icon-creditcard.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/icon-creditcard.png diff --git a/server/src/public/businesswoman/useCases/airtravel/airtravel-beach.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-beach.svg similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/airtravel-beach.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-beach.svg diff --git a/server/src/public/businesswoman/useCases/airtravel/airtravel-calendar.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-calendar.svg similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/airtravel-calendar.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-calendar.svg diff --git a/server/src/public/businesswoman/useCases/airtravel/airtravel-card.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-card.svg similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/airtravel-card.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-card.svg diff --git a/server/src/public/businesswoman/useCases/airtravel/airtravel-details.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-details.svg similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/airtravel-details.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-details.svg diff --git a/server/src/public/businesswoman/useCases/airtravel/airtravel-laptop.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-laptop.svg similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/airtravel-laptop.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-laptop.svg diff --git a/server/src/public/businesswoman/useCases/airtravel/airtravel-secure.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-secure.svg similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/airtravel-secure.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/airtravel-secure.svg diff --git a/server/src/public/businesswoman/useCases/airtravel/icon-airline.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/icon-airline.png similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/icon-airline.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/icon-airline.png diff --git a/server/src/public/businesswoman/useCases/airtravel/icon-airplane.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/icon-airplane.png similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/icon-airplane.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/icon-airplane.png diff --git a/server/src/public/businesswoman/useCases/airtravel/icon-ticket.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/icon-ticket.png similarity index 100% rename from server/src/public/businesswoman/useCases/airtravel/icon-ticket.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/airtravel/icon-ticket.png diff --git a/server/src/public/businesswoman/useCases/conference/conference-card.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-card.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-card.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-card.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-connected.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-connected.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-connected.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-connected.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-laptop.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-laptop.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-laptop.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-laptop.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-line-waiting.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-line-waiting.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-line-waiting.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-line-waiting.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-phone-attributes.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-phone-attributes.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-phone-attributes.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-phone-attributes.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-phone-notification.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-phone-notification.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-phone-notification.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-phone-notification.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-safe.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-safe.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-safe.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-safe.svg diff --git a/server/src/public/businesswoman/useCases/conference/conference-share-information.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-share-information.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/conference-share-information.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/conference-share-information.svg diff --git a/server/src/public/businesswoman/useCases/conference/icon-conference-pass.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-conference-pass.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/icon-conference-pass.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-conference-pass.svg diff --git a/server/src/public/businesswoman/useCases/conference/icon-conference.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-conference.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/icon-conference.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-conference.svg diff --git a/server/src/public/businesswoman/useCases/conference/icon-dave.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-dave.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/icon-dave.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-dave.svg diff --git a/server/src/public/businesswoman/useCases/conference/icon-employee.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-employee.svg similarity index 100% rename from server/src/public/businesswoman/useCases/conference/icon-employee.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/icon-employee.svg diff --git a/server/src/public/businesswoman/useCases/conference/logo-conference.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/logo-conference.png similarity index 100% rename from server/src/public/businesswoman/useCases/conference/logo-conference.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/conference/logo-conference.png diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-card.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-card.svg similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-card.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-card.svg diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-counter.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-counter.svg similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-counter.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-counter.svg diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-icon-keycard.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-icon-keycard.png similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-icon-keycard.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-icon-keycard.png diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-keycard.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-keycard.svg similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-keycard.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-keycard.svg diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-logo-hotel.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-logo-hotel.png similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-logo-hotel.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-logo-hotel.png diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-qr.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-qr.svg similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-qr.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-qr.svg diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-sleep.svg b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-sleep.svg similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-sleep.svg rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-sleep.svg diff --git a/server/src/public/businesswoman/useCases/hotel/hotel-start.png b/apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-start.png similarity index 100% rename from server/src/public/businesswoman/useCases/hotel/hotel-start.png rename to apps/bc-wallet-demo-server/src/public/businesswoman/useCases/hotel/hotel-start.png diff --git a/server/src/public/common/app-store-screenshots.png b/apps/bc-wallet-demo-server/src/public/common/app-store-screenshots.png similarity index 100% rename from server/src/public/common/app-store-screenshots.png rename to apps/bc-wallet-demo-server/src/public/common/app-store-screenshots.png diff --git a/server/src/public/common/getStarted.svg b/apps/bc-wallet-demo-server/src/public/common/getStarted.svg similarity index 91% rename from server/src/public/common/getStarted.svg rename to apps/bc-wallet-demo-server/src/public/common/getStarted.svg index bfb9a2581..b718b3a82 100644 --- a/server/src/public/common/getStarted.svg +++ b/apps/bc-wallet-demo-server/src/public/common/getStarted.svg @@ -1,34 +1,34 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/client/src/assets/dark/icon-balloon-dark.svg b/apps/bc-wallet-demo-server/src/public/common/icon-balloon-dark.svg similarity index 100% rename from client/src/assets/dark/icon-balloon-dark.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-balloon-dark.svg diff --git a/client/src/assets/light/icon-balloon-light.svg b/apps/bc-wallet-demo-server/src/public/common/icon-balloon-light.svg similarity index 100% rename from client/src/assets/light/icon-balloon-light.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-balloon-light.svg diff --git a/client/src/assets/dark/icon-moon-dark.svg b/apps/bc-wallet-demo-server/src/public/common/icon-moon-dark.svg similarity index 100% rename from client/src/assets/dark/icon-moon-dark.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-moon-dark.svg diff --git a/client/src/assets/light/icon-moon-light.svg b/apps/bc-wallet-demo-server/src/public/common/icon-moon-light.svg similarity index 100% rename from client/src/assets/light/icon-moon-light.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-moon-light.svg diff --git a/client/src/assets/dark/icon-notification-dark.svg b/apps/bc-wallet-demo-server/src/public/common/icon-notification-dark.svg similarity index 100% rename from client/src/assets/dark/icon-notification-dark.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-notification-dark.svg diff --git a/client/src/assets/light/icon-notification-light.svg b/apps/bc-wallet-demo-server/src/public/common/icon-notification-light.svg similarity index 100% rename from client/src/assets/light/icon-notification-light.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-notification-light.svg diff --git a/client/src/assets/dark/icon-person-dark.svg b/apps/bc-wallet-demo-server/src/public/common/icon-person-dark.svg similarity index 100% rename from client/src/assets/dark/icon-person-dark.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-person-dark.svg diff --git a/client/src/assets/light/icon-person-light.svg b/apps/bc-wallet-demo-server/src/public/common/icon-person-light.svg similarity index 100% rename from client/src/assets/light/icon-person-light.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-person-light.svg diff --git a/client/src/assets/dark/icon-wallet-dark.svg b/apps/bc-wallet-demo-server/src/public/common/icon-wallet-dark.svg similarity index 100% rename from client/src/assets/dark/icon-wallet-dark.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-wallet-dark.svg diff --git a/client/src/assets/light/icon-wallet-light.svg b/apps/bc-wallet-demo-server/src/public/common/icon-wallet-light.svg similarity index 100% rename from client/src/assets/light/icon-wallet-light.svg rename to apps/bc-wallet-demo-server/src/public/common/icon-wallet-light.svg diff --git a/server/src/public/common/onboarding-completed-light.svg b/apps/bc-wallet-demo-server/src/public/common/onboarding-completed-light.svg similarity index 99% rename from server/src/public/common/onboarding-completed-light.svg rename to apps/bc-wallet-demo-server/src/public/common/onboarding-completed-light.svg index 714b18e5e..7f5bd6013 100644 --- a/server/src/public/common/onboarding-completed-light.svg +++ b/apps/bc-wallet-demo-server/src/public/common/onboarding-completed-light.svg @@ -1,7 +1,7 @@ - + + + diff --git a/client/public/logo192.png b/apps/bc-wallet-demo-web/public/logo192.png similarity index 100% rename from client/public/logo192.png rename to apps/bc-wallet-demo-web/public/logo192.png diff --git a/client/public/logo512.png b/apps/bc-wallet-demo-web/public/logo512.png similarity index 100% rename from client/public/logo512.png rename to apps/bc-wallet-demo-web/public/logo512.png diff --git a/client/public/robots.txt b/apps/bc-wallet-demo-web/public/robots.txt similarity index 100% rename from client/public/robots.txt rename to apps/bc-wallet-demo-web/public/robots.txt diff --git a/client/public/seo-logo.jpg b/apps/bc-wallet-demo-web/public/seo-logo.jpg similarity index 100% rename from client/public/seo-logo.jpg rename to apps/bc-wallet-demo-web/public/seo-logo.jpg diff --git a/client/public/sitemap.xml b/apps/bc-wallet-demo-web/public/sitemap.xml similarity index 100% rename from client/public/sitemap.xml rename to apps/bc-wallet-demo-web/public/sitemap.xml diff --git a/client/src/App.tsx b/apps/bc-wallet-demo-web/src/App.tsx similarity index 74% rename from client/src/App.tsx rename to apps/bc-wallet-demo-web/src/App.tsx index f8d43b710..193019034 100644 --- a/client/src/App.tsx +++ b/apps/bc-wallet-demo-web/src/App.tsx @@ -1,17 +1,15 @@ -import type { Socket } from 'socket.io-client' - -import { AnimatePresence } from 'framer-motion' import { useEffect, useState } from 'react' -import { Routes, Route, useLocation, useNavigate, Navigate } from 'react-router-dom' +import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom' +import { AnimatePresence } from 'framer-motion' +import type { Socket } from 'socket.io-client' import { io } from 'socket.io-client' - -import { baseWsUrl, socketPath } from './api/BaseUrl' +import { demoBackendBaseWsUrl, demoBackendSocketPath } from './api/BaseUrl' import { useAppDispatch } from './hooks/hooks' import { useAnalytics } from './hooks/useAnalytics' -import { PageNotFound } from './pages/PageNotFound' import { DashboardPage } from './pages/dashboard/DashboardPage' import { LandingPage } from './pages/landing/LandingPage' import { OnboardingPage } from './pages/onboarding/OnboardingPage' +import { PageNotFound } from './pages/PageNotFound' import { UseCasePage } from './pages/useCase/UseCasePage' import { useConnection } from './slices/connection/connectionSelectors' import { usePreferences } from './slices/preferences/preferencesSelectors' @@ -20,8 +18,8 @@ import { fetchLastServerReset } from './slices/preferences/preferencesThunks' import { setMessage } from './slices/socket/socketSlice' import { AuthProvider } from './utils/AuthContext' import { basePath } from './utils/BasePath' -import { PrivateRoute } from './utils/PrivateRoute' import { ThemeProvider } from './utils/ThemeContext' +import { getTenantIdFromPath } from './utils/Helpers' function App() { useAnalytics() @@ -34,6 +32,7 @@ function App() { const localStorageTheme = localStorage.theme === 'dark' const windowMedia = !('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches + const tenantId = getTenantIdFromPath(); useEffect(() => { if (localStorageTheme || windowMedia) { @@ -57,7 +56,7 @@ function App() { }, [connectionDate, lastServerReset]) useEffect(() => { - const ws = io(baseWsUrl, { path: socketPath }) + const ws = io(demoBackendBaseWsUrl, { path: demoBackendSocketPath }) ws.on('connect', () => { setSocket(ws) }) @@ -77,26 +76,10 @@ function App() { {basePath !== '/' && }>} - } /> - } /> - } /> - } /> - - - - } - /> - - - - } - /> + } /> + } /> + } /> + } /> } /> diff --git a/client/src/FramerAnimations.tsx b/apps/bc-wallet-demo-web/src/FramerAnimations.tsx similarity index 100% rename from client/src/FramerAnimations.tsx rename to apps/bc-wallet-demo-web/src/FramerAnimations.tsx diff --git a/apps/bc-wallet-demo-web/src/api/BaseUrl.ts b/apps/bc-wallet-demo-web/src/api/BaseUrl.ts new file mode 100644 index 000000000..faf2e5212 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/api/BaseUrl.ts @@ -0,0 +1,53 @@ +import axios from 'axios' +import { setupCache } from 'axios-cache-interceptor' +import type { CacheOptions } from 'axios-cache-interceptor/src/cache/create' + +import { getTenantIdFromPath } from '../utils/Helpers' + +declare global { + interface Window { + __env?: { + REACT_APP_DEMO_API_URL?: string + REACT_APP_BASE_ROUTE?: string + REACT_APP_SHOWCASE_API_URL?: string + } + } +} + +const getEnv = (key: string, fallback: string): string => { + if (window.__env && window.__env[key as keyof typeof window.__env]) { + return window.__env[key as keyof typeof window.__env] || fallback + } + console.warn(`Using process.env var for ${key}. This should only happen in dev environments`) + return process.env[key] || fallback +} + +const cacheOptions: CacheOptions = { + ttl: 30 * 24 * 60 * 60 * 1000, // keep 30 days + methods: ['get'], + cachePredicate: (response) => { + return response.status >= 200 && response.status < 400 + }, +} + +export const demoBackendBaseRoute = getEnv('REACT_APP_BASE_ROUTE', '/digital-trust/showcase') +export const demoBackendBaseWsUrl = getEnv('REACT_APP_DEMO_API_URL', '') +export const demoBackendBaseUrl = demoBackendBaseWsUrl + demoBackendBaseRoute +export const demoBackendSocketPath = `${demoBackendBaseRoute}/socket/` +export const demoBackendApi = setupCache( + axios.create({ + baseURL: demoBackendBaseUrl, + }), + cacheOptions, +) + +const tenantId = getTenantIdFromPath() +const base = getEnv('REACT_APP_SHOWCASE_API_URL', '') +export const showcaseServerBaseUrl = tenantId ? `${base}/${tenantId}` : base + +export const showcaseApi = setupCache( + axios.create({ + baseURL: showcaseServerBaseUrl, + }), + cacheOptions, +) diff --git a/client/src/api/ConnectionApi.ts b/apps/bc-wallet-demo-web/src/api/ConnectionApi.ts similarity index 60% rename from client/src/api/ConnectionApi.ts rename to apps/bc-wallet-demo-web/src/api/ConnectionApi.ts index d27801f47..79a50c481 100644 --- a/client/src/api/ConnectionApi.ts +++ b/apps/bc-wallet-demo-web/src/api/ConnectionApi.ts @@ -1,23 +1,25 @@ import type { AxiosResponse } from 'axios' -import { apiCall } from './BaseUrl' +import { demoBackendApi } from './BaseUrl' export const createInvitation = ( agentName?: string, goalCode?: string, + goal?: string, agentImageUrl?: string ): Promise => { - return apiCall.post('/demo/connections/createInvite', { + return demoBackendApi.post('/demo/connections/createInvite', { my_label: agentName, image_url: agentImageUrl, goal_code: goalCode, + goal }) } export const getConnectionByInvitation = (invitationMsgId: string): Promise => { - return apiCall.get(`/demo/connections/invitationId/${invitationMsgId}`) + return demoBackendApi.get(`/demo/connections/invitationId/${invitationMsgId}`) } export const getConnectionById = (connectionId: string): Promise => { - return apiCall.get(`/demo/connections/getConnectionStatus/${connectionId}`) + return demoBackendApi.get(`/demo/connections/getConnectionStatus/${connectionId}`) } diff --git a/apps/bc-wallet-demo-web/src/api/CredentialApi.ts b/apps/bc-wallet-demo-web/src/api/CredentialApi.ts new file mode 100644 index 000000000..7e6399903 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/api/CredentialApi.ts @@ -0,0 +1,39 @@ +import { demoBackendApi } from './BaseUrl' +import type { AxiosResponse } from 'axios' +import type { CredentialDefinition } from '../slices/types' + +export const issueCredential = async ( + connectionId: string, + credentialDefinition: CredentialDefinition +): Promise => { + return demoBackendApi.post(`/demo/credentials/offerCredential`, { + connection_id: connectionId, + cred_def_id: credentialDefinition.identifier, + credential_proposal: { + '@type': 'issue-credential/1.0/credential-preview', + attributes: credentialDefinition.schema.attributes, + }, + }) +} + +export const issueDeepCredential = async ( + connectionId: string, + credentialDefinition: CredentialDefinition +): Promise => { + return demoBackendApi.post(`/demo/deeplink/offerCredential`, { + connection_id: connectionId, + cred_def_id: credentialDefinition.identifier, + credential_proposal: { + '@type': 'issue-credential/1.0/credential-preview', + attributes: credentialDefinition.schema.attributes, + }, + }) +} + +export const getCredentialById = (credentialId: string): Promise => { + return demoBackendApi.get(`/demo/credentials/${credentialId}`) +} + +export const deleteCredentialById = (credentialId: string): Promise => { + return demoBackendApi.delete(`/demo/credentials/${credentialId}`) +} diff --git a/client/src/api/ProofApi.ts b/apps/bc-wallet-demo-web/src/api/ProofApi.ts similarity index 82% rename from client/src/api/ProofApi.ts rename to apps/bc-wallet-demo-web/src/api/ProofApi.ts index fd6be65c6..f9e501766 100644 --- a/client/src/api/ProofApi.ts +++ b/apps/bc-wallet-demo-web/src/api/ProofApi.ts @@ -1,7 +1,7 @@ -import type { ProofRequestData } from '../slices/types' import type { AxiosResponse } from 'axios' -import { apiCall } from './BaseUrl' +import type { ProofRequestData } from '../slices/types' +import { demoBackendApi } from './BaseUrl' export const createProofRequest = (data: ProofRequestData): Promise => { const proofRequest = { @@ -11,7 +11,7 @@ export const createProofRequest = (data: ProofRequestData): Promise => { - return apiCall.get(`/demo/proofs/${proofId}`) + return demoBackendApi.get(`/demo/proofs/${proofId}`) } export const deleteProofById = (proofId: string): Promise => { - return apiCall.delete(`/demo/proofs/${proofId}`) + return demoBackendApi.delete(`/demo/proofs/${proofId}`) } diff --git a/client/src/api/RevocationApi.ts b/apps/bc-wallet-demo-web/src/api/RevocationApi.ts similarity index 78% rename from client/src/api/RevocationApi.ts rename to apps/bc-wallet-demo-web/src/api/RevocationApi.ts index a9f64b70f..5956aad9e 100644 --- a/client/src/api/RevocationApi.ts +++ b/apps/bc-wallet-demo-web/src/api/RevocationApi.ts @@ -1,10 +1,8 @@ import type { RevocationRecord } from '../slices/types' -import type { AxiosResponse } from 'axios' - -import { apiCall } from './BaseUrl' +import { demoBackendApi } from './BaseUrl' export const revokeCredential = (record: RevocationRecord) => { - return apiCall.post('/demo/revoke', { + return demoBackendApi.post('/demo/revoke', { comment: 'Credential Revoked', connection_id: record.connectionId, cred_rev_id: record.credRevocationId, diff --git a/apps/bc-wallet-demo-web/src/api/ShowcaseApi.ts b/apps/bc-wallet-demo-web/src/api/ShowcaseApi.ts new file mode 100644 index 000000000..1220a9b47 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/api/ShowcaseApi.ts @@ -0,0 +1,8 @@ +import type { AxiosResponse } from 'axios' + +import type { ShowcaseResponse } from 'bc-wallet-openapi' +import { showcaseApi } from './BaseUrl' + +export const getShowcaseBySlug = async (slug: string): Promise> => { + return showcaseApi.get(`/showcases/${slug}`) +} diff --git a/client/src/api/UseCaseApi.ts b/apps/bc-wallet-demo-web/src/api/UseCaseApi.ts similarity index 55% rename from client/src/api/UseCaseApi.ts rename to apps/bc-wallet-demo-web/src/api/UseCaseApi.ts index bc4803a8b..17e3d6294 100644 --- a/client/src/api/UseCaseApi.ts +++ b/apps/bc-wallet-demo-web/src/api/UseCaseApi.ts @@ -1,11 +1,11 @@ import type { AxiosResponse } from 'axios' -import { apiCall } from './BaseUrl' +import { demoBackendApi } from './BaseUrl' export const getUseCasesByCharType = (type: string): Promise => { - return apiCall.get(`/demo/usecases/character/${type}`, {}) + return demoBackendApi.get(`/demo/usecases/character/${type}`, {}) } export const getUseCaseBySlug = (slug: string): Promise => { - return apiCall.get(`/demo/usecases/${slug}`) + return demoBackendApi.get(`/demo/usecases/${slug}`) } diff --git a/client/src/api/UtilApi.ts b/apps/bc-wallet-demo-web/src/api/UtilApi.ts similarity index 54% rename from client/src/api/UtilApi.ts rename to apps/bc-wallet-demo-web/src/api/UtilApi.ts index e7126e7cc..164fe659d 100644 --- a/client/src/api/UtilApi.ts +++ b/apps/bc-wallet-demo-web/src/api/UtilApi.ts @@ -1,7 +1,7 @@ import type { AxiosResponse } from 'axios' -import { apiCall } from './BaseUrl' +import { demoBackendApi } from './BaseUrl' export const getLastServerReset = (): Promise => { - return apiCall.get('/server/last-reset') + return demoBackendApi.get('/server/last-reset') } diff --git a/client/src/api/WalletApi.ts b/apps/bc-wallet-demo-web/src/api/WalletApi.ts similarity index 54% rename from client/src/api/WalletApi.ts rename to apps/bc-wallet-demo-web/src/api/WalletApi.ts index 5a44516a3..87dceabac 100644 --- a/client/src/api/WalletApi.ts +++ b/apps/bc-wallet-demo-web/src/api/WalletApi.ts @@ -1,7 +1,7 @@ import type { AxiosResponse } from 'axios' -import { apiCall } from './BaseUrl' +import { demoBackendApi } from './BaseUrl' export const getWallets = (): Promise => { - return apiCall.get('/demo/wallets') + return demoBackendApi.get('/demo/wallets') } diff --git a/apps/bc-wallet-demo-web/src/api/credentialDefinitionApi.ts b/apps/bc-wallet-demo-web/src/api/credentialDefinitionApi.ts new file mode 100644 index 000000000..b95899419 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/api/credentialDefinitionApi.ts @@ -0,0 +1,8 @@ +import type { AxiosResponse } from 'axios' + +import type { CredentialDefinitionResponse } from 'bc-wallet-openapi' +import { showcaseApi } from './BaseUrl' + +export const getCredentialDefinitionById = async (id: string): Promise> => { + return showcaseApi.get(`/credentials/definitions/${id}`) +} diff --git a/client/src/assets/Line.tsx b/apps/bc-wallet-demo-web/src/assets/Line.tsx similarity index 100% rename from client/src/assets/Line.tsx rename to apps/bc-wallet-demo-web/src/assets/Line.tsx diff --git a/client/src/assets/dark/TriangleDark.tsx b/apps/bc-wallet-demo-web/src/assets/dark/TriangleDark.tsx similarity index 100% rename from client/src/assets/dark/TriangleDark.tsx rename to apps/bc-wallet-demo-web/src/assets/dark/TriangleDark.tsx diff --git a/client/src/assets/dark/animo-home-illustration-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/animo-home-illustration-dark.svg similarity index 100% rename from client/src/assets/dark/animo-home-illustration-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/animo-home-illustration-dark.svg diff --git a/client/src/assets/dark/animo-logo-dark.png b/apps/bc-wallet-demo-web/src/assets/dark/animo-logo-dark.png similarity index 100% rename from client/src/assets/dark/animo-logo-dark.png rename to apps/bc-wallet-demo-web/src/assets/dark/animo-logo-dark.png diff --git a/client/src/assets/dark/animo-triangle-logo-dark.png b/apps/bc-wallet-demo-web/src/assets/dark/animo-triangle-logo-dark.png similarity index 100% rename from client/src/assets/dark/animo-triangle-logo-dark.png rename to apps/bc-wallet-demo-web/src/assets/dark/animo-triangle-logo-dark.png diff --git a/client/src/assets/dark/app-store-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/app-store-dark.svg similarity index 87% rename from client/src/assets/dark/app-store-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/app-store-dark.svg index 0b07a9f3b..c3c9881ff 100644 --- a/client/src/assets/dark/app-store-dark.svg +++ b/apps/bc-wallet-demo-web/src/assets/dark/app-store-dark.svg @@ -1,48 +1,48 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/assets/dark/documents-rounded-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/documents-rounded-dark.svg similarity index 100% rename from client/src/assets/dark/documents-rounded-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/documents-rounded-dark.svg diff --git a/client/src/assets/dark/icon-arrow-light.png b/apps/bc-wallet-demo-web/src/assets/dark/icon-arrow-light.png similarity index 100% rename from client/src/assets/dark/icon-arrow-light.png rename to apps/bc-wallet-demo-web/src/assets/dark/icon-arrow-light.png diff --git a/server/src/public/common/icon-balloon-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-balloon-dark.svg similarity index 100% rename from server/src/public/common/icon-balloon-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-balloon-dark.svg diff --git a/server/src/public/common/icon-moon-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-moon-dark.svg similarity index 100% rename from server/src/public/common/icon-moon-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-moon-dark.svg diff --git a/server/src/public/common/icon-notification-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-notification-dark.svg similarity index 100% rename from server/src/public/common/icon-notification-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-notification-dark.svg diff --git a/server/src/public/common/icon-person-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-person-dark.svg similarity index 100% rename from server/src/public/common/icon-person-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-person-dark.svg diff --git a/client/src/assets/dark/icon-qr-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-qr-dark.svg similarity index 100% rename from client/src/assets/dark/icon-qr-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-qr-dark.svg diff --git a/server/src/public/common/icon-wallet-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-wallet-dark.svg similarity index 100% rename from server/src/public/common/icon-wallet-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-wallet-dark.svg diff --git a/client/src/assets/dark/icon-welcome-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/icon-welcome-dark.svg similarity index 100% rename from client/src/assets/dark/icon-welcome-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/icon-welcome-dark.svg diff --git a/client/src/assets/dark/onboarding-choose-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/onboarding-choose-dark.svg similarity index 100% rename from client/src/assets/dark/onboarding-choose-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/onboarding-choose-dark.svg diff --git a/client/src/assets/dark/onboarding-completed-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/onboarding-completed-dark.svg similarity index 100% rename from client/src/assets/dark/onboarding-completed-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/onboarding-completed-dark.svg diff --git a/client/src/assets/dark/onboarding-connect-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/onboarding-connect-dark.svg similarity index 100% rename from client/src/assets/dark/onboarding-connect-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/onboarding-connect-dark.svg diff --git a/client/src/assets/dark/onboarding-secure-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/onboarding-secure-dark.svg similarity index 100% rename from client/src/assets/dark/onboarding-secure-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/onboarding-secure-dark.svg diff --git a/client/src/assets/dark/onboarding-started-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/onboarding-started-dark.svg similarity index 100% rename from client/src/assets/dark/onboarding-started-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/onboarding-started-dark.svg diff --git a/client/src/assets/dark/onboarding-wallet-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/onboarding-wallet-dark.svg similarity index 100% rename from client/src/assets/dark/onboarding-wallet-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/onboarding-wallet-dark.svg diff --git a/client/src/assets/dark/phone-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/phone-dark.svg similarity index 100% rename from client/src/assets/dark/phone-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/phone-dark.svg diff --git a/client/src/assets/dark/phone-rounded-dark.svg b/apps/bc-wallet-demo-web/src/assets/dark/phone-rounded-dark.svg similarity index 100% rename from client/src/assets/dark/phone-rounded-dark.svg rename to apps/bc-wallet-demo-web/src/assets/dark/phone-rounded-dark.svg diff --git a/client/src/assets/light/TriangleLight.tsx b/apps/bc-wallet-demo-web/src/assets/light/TriangleLight.tsx similarity index 100% rename from client/src/assets/light/TriangleLight.tsx rename to apps/bc-wallet-demo-web/src/assets/light/TriangleLight.tsx diff --git a/client/src/assets/light/animo-home-illustration-light.svg b/apps/bc-wallet-demo-web/src/assets/light/animo-home-illustration-light.svg similarity index 100% rename from client/src/assets/light/animo-home-illustration-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/animo-home-illustration-light.svg diff --git a/client/src/assets/light/animo-logo-light.png b/apps/bc-wallet-demo-web/src/assets/light/animo-logo-light.png similarity index 100% rename from client/src/assets/light/animo-logo-light.png rename to apps/bc-wallet-demo-web/src/assets/light/animo-logo-light.png diff --git a/client/src/assets/light/animo-triangle-logo-light.png b/apps/bc-wallet-demo-web/src/assets/light/animo-triangle-logo-light.png similarity index 100% rename from client/src/assets/light/animo-triangle-logo-light.png rename to apps/bc-wallet-demo-web/src/assets/light/animo-triangle-logo-light.png diff --git a/client/src/assets/light/app-store-badge.svg b/apps/bc-wallet-demo-web/src/assets/light/app-store-badge.svg similarity index 100% rename from client/src/assets/light/app-store-badge.svg rename to apps/bc-wallet-demo-web/src/assets/light/app-store-badge.svg diff --git a/client/src/assets/light/documents-rounded-light.svg b/apps/bc-wallet-demo-web/src/assets/light/documents-rounded-light.svg similarity index 100% rename from client/src/assets/light/documents-rounded-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/documents-rounded-light.svg diff --git a/client/src/assets/light/google-play-badge.png b/apps/bc-wallet-demo-web/src/assets/light/google-play-badge.png similarity index 100% rename from client/src/assets/light/google-play-badge.png rename to apps/bc-wallet-demo-web/src/assets/light/google-play-badge.png diff --git a/client/src/assets/light/icon-app-store.png b/apps/bc-wallet-demo-web/src/assets/light/icon-app-store.png similarity index 100% rename from client/src/assets/light/icon-app-store.png rename to apps/bc-wallet-demo-web/src/assets/light/icon-app-store.png diff --git a/client/src/assets/light/icon-arrow-dark.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-arrow-dark.svg similarity index 100% rename from client/src/assets/light/icon-arrow-dark.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-arrow-dark.svg diff --git a/server/src/public/common/icon-balloon-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-balloon-light.svg similarity index 100% rename from server/src/public/common/icon-balloon-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-balloon-light.svg diff --git a/server/src/public/common/icon-moon-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-moon-light.svg similarity index 100% rename from server/src/public/common/icon-moon-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-moon-light.svg diff --git a/server/src/public/common/icon-notification-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-notification-light.svg similarity index 100% rename from server/src/public/common/icon-notification-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-notification-light.svg diff --git a/server/src/public/common/icon-person-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-person-light.svg similarity index 100% rename from server/src/public/common/icon-person-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-person-light.svg diff --git a/client/src/assets/light/icon-play-store.png b/apps/bc-wallet-demo-web/src/assets/light/icon-play-store.png similarity index 100% rename from client/src/assets/light/icon-play-store.png rename to apps/bc-wallet-demo-web/src/assets/light/icon-play-store.png diff --git a/client/src/assets/light/icon-qr-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-qr-light.svg similarity index 100% rename from client/src/assets/light/icon-qr-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-qr-light.svg diff --git a/server/src/public/common/icon-wallet-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-wallet-light.svg similarity index 100% rename from server/src/public/common/icon-wallet-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-wallet-light.svg diff --git a/client/src/assets/light/icon-welcome-light.svg b/apps/bc-wallet-demo-web/src/assets/light/icon-welcome-light.svg similarity index 100% rename from client/src/assets/light/icon-welcome-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/icon-welcome-light.svg diff --git a/apps/bc-wallet-demo-web/src/assets/light/landing-screen.svg b/apps/bc-wallet-demo-web/src/assets/light/landing-screen.svg new file mode 100755 index 000000000..070f30695 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/assets/light/landing-screen.svg @@ -0,0 +1,2 @@ + +BestBC \ No newline at end of file diff --git a/client/src/assets/light/onboarding-choose-light.svg b/apps/bc-wallet-demo-web/src/assets/light/onboarding-choose-light.svg similarity index 100% rename from client/src/assets/light/onboarding-choose-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/onboarding-choose-light.svg diff --git a/server/src/public/student/onboarding-connect-light.svg b/apps/bc-wallet-demo-web/src/assets/light/onboarding-connect-light.svg similarity index 100% rename from server/src/public/student/onboarding-connect-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/onboarding-connect-light.svg diff --git a/server/src/public/common/onboarding-credential-light.svg b/apps/bc-wallet-demo-web/src/assets/light/onboarding-credential-light.svg similarity index 99% rename from server/src/public/common/onboarding-credential-light.svg rename to apps/bc-wallet-demo-web/src/assets/light/onboarding-credential-light.svg index d3d720dfe..09a418d86 100644 --- a/server/src/public/common/onboarding-credential-light.svg +++ b/apps/bc-wallet-demo-web/src/assets/light/onboarding-credential-light.svg @@ -1,7 +1,7 @@ - +
+
+

404

+

{resourceType || 'Page'} Not Found

+
+
+ ) + } + + // Enhanced version when resourceType is provided + return ( + +
+

404

+

{resourceType || 'Page'} Not Found

+ {resourceName && ( +

+ A {(resourceType || 'page').toLowerCase()} named "{resourceName}" could not be found. +

+ )} + +
+
+ ) +} diff --git a/client/src/pages/dashboard/DashboardPage.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/DashboardPage.tsx similarity index 51% rename from client/src/pages/dashboard/DashboardPage.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/DashboardPage.tsx index 28b785dc4..0800d6a8c 100644 --- a/client/src/pages/dashboard/DashboardPage.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/DashboardPage.tsx @@ -1,58 +1,84 @@ -import type { CustomCharacter } from '../../slices/types' - +import React, { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import { trackPageView } from '@snowplow/browser-tracker' -import { AnimatePresence, motion } from 'framer-motion' +import { motion, AnimatePresence } from 'framer-motion' import { track } from 'insights-js' -import React, { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' - -import { page } from '../../FramerAnimations' import { Modal } from '../../components/Modal' +import { page } from '../../FramerAnimations' import { useAppDispatch } from '../../hooks/hooks' import { useTitle } from '../../hooks/useTitle' -import { useCurrentCharacter } from '../../slices/characters/charactersSelectors' import { useCredentials } from '../../slices/credentials/credentialsSelectors' import { usePreferences } from '../../slices/preferences/preferencesSelectors' import { setDemoCompleted } from '../../slices/preferences/preferencesSlice' -import { useSocket } from '../../slices/socket/socketSelector' +import { useShowcases } from '../../slices/showcases/showcasesSelectors' import { basePath } from '../../utils/BasePath' import { Footer } from '../landing/components/Footer' import { NavBar } from '../landing/components/Navbar' - import { DemoCompletedModal } from './components/DemoCompletedModal' import { ProfileCard } from './components/ProfileCard' -import { RevocationContainer } from './components/RevocationContainer' import { UseCaseContainer } from './components/UseCaseContainer' +import { fetchPersonaBySlug, fetchShowcaseBySlug } from '../../slices/showcases/showcasesThunks' +import { clearShowcase } from '../../slices/showcases/showcasesSlice' +import { usePersonaSlug, useSlug } from '../../utils/SlugUtils' +import { PageNotFound } from '../PageNotFound' +import { ScenarioType } from 'bc-wallet-openapi' +import type { Scenario } from '../../slices/types' export const DashboardPage: React.FC = () => { useTitle('Dashboard | BC Wallet Self-Sovereign Identity Demo') - const navigate = useNavigate() const dispatch = useAppDispatch() - const socket = useSocket() - const { issuedCredentials, revokableCredentials } = useCredentials() - const { completedUseCaseSlugs, demoCompleted, completeCanceled, revocationEnabled, showHiddenUseCases } = - usePreferences() - const currentCharacter = { - ...useCurrentCharacter(), - useCases: useCurrentCharacter()?.useCases.filter((item) => !item.hidden || showHiddenUseCases) ?? [], - } as CustomCharacter - const useCases = currentCharacter?.useCases + const { revokableCredentials } = useCredentials() + const showcaseSlug = useSlug() + const personaSlug = usePersonaSlug() + const { showcase, currentPersona } = useShowcases() + const { completedUseCaseSlugs, demoCompleted, completeCanceled, revocationEnabled, showHiddenUseCases } = usePreferences() + const [scenarios, setScenarios] = useState([]) useEffect(() => { - if (completedUseCaseSlugs.length !== 0 && completedUseCaseSlugs.length === useCases?.length && !completeCanceled) { + if (completedUseCaseSlugs.length !== 0 && completedUseCaseSlugs.length === scenarios?.length && !completeCanceled) { dispatch(setDemoCompleted(true)) } - }, [completedUseCaseSlugs]) + }, [scenarios, completedUseCaseSlugs]) + + useEffect(() => { + const load = async () => { + dispatch(clearShowcase()) + await dispatch(fetchShowcaseBySlug(showcaseSlug)) + await dispatch(fetchPersonaBySlug(personaSlug)) + } + void load() + }, [dispatch, showcaseSlug, personaSlug]) useEffect(() => { trackPageView() }, []) + useEffect(() => { + if (!showcase || !currentPersona) { + return + } + const result = showcase.scenarios.filter(scenario => scenario.persona.id === currentPersona.id && scenario.type === ScenarioType.Presentation ) + setScenarios(result) + }, [showcase, currentPersona]) + + + if (showcase === undefined || currentPersona === undefined) { + return
Loading...
+ } + + if (showcase === null) { + return + } + + if (currentPersona === null) { + return + } + const ERROR_TITLE = `Woops...` const ERROR_DESCRIPTION = `That's not gone well. Please restart the demo.` const routeError = () => { - navigate(`${basePath}/demo`) + navigate(`${basePath}/`) dispatch({ type: 'demo/RESET' }) } @@ -60,11 +86,11 @@ export const DashboardPage: React.FC = () => { navigate(`${basePath}/`) dispatch({ type: 'demo/RESET' }) - if (currentCharacter) { + if (currentPersona) { track({ id: 'demo-character-completed', parameters: { - character: currentCharacter.name, + character: currentPersona.name, }, }) } @@ -85,24 +111,26 @@ export const DashboardPage: React.FC = () => {
- {currentCharacter ? ( + {currentPersona && showcase ? ( <>
- {revokableCredentials.length > 0 && revocationEnabled && currentCharacter.revocationInfo && ( - - )} + {/*FIXME we need to add support back for revocations*/} + {/*{revokableCredentials.length > 0 && revocationEnabled && currentPersona.revocationInfo && (*/} + {/* */} + {/*)}*/}
- +
diff --git a/client/src/pages/dashboard/components/DashboardCard.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/DashboardCard.tsx similarity index 100% rename from client/src/pages/dashboard/components/DashboardCard.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/components/DashboardCard.tsx index d1b2b7a64..43227ba76 100644 --- a/client/src/pages/dashboard/components/DashboardCard.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/DashboardCard.tsx @@ -1,7 +1,7 @@ import type { ReactElement } from 'react' +import React from 'react' import { motion } from 'framer-motion' -import React from 'react' import { fade } from '../../../FramerAnimations' diff --git a/client/src/pages/dashboard/components/DemoCompletedModal.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/DemoCompletedModal.tsx similarity index 71% rename from client/src/pages/dashboard/components/DemoCompletedModal.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/components/DemoCompletedModal.tsx index 142b50d97..d882ed0d7 100644 --- a/client/src/pages/dashboard/components/DemoCompletedModal.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/DemoCompletedModal.tsx @@ -1,18 +1,15 @@ -import { AnimatePresence, motion } from 'framer-motion' -import { useNavigate } from 'react-router-dom' +import { motion, AnimatePresence } from 'framer-motion' -import { standardFade, dropIn } from '../../../FramerAnimations' -import { Button } from '../../../components/Button' import { SmallButton } from '../../../components/SmallButton' import { SmallButtonText } from '../../../components/SmallButtonText' +import { dropIn, standardFade } from '../../../FramerAnimations' + export interface Props { action(): void cancel?(): void } export const DemoCompletedModal: React.FC = ({ action, cancel }) => { - const navigate = useNavigate() - const TITLE = 'Showcase Completed!' const DESCRIPTION = 'Looking to leave some feedback? Take our survey and connect with us!' @@ -36,7 +33,7 @@ export const DemoCompletedModal: React.FC = ({ action, cancel }) => { initial="hidden" animate="show" exit="exit" - className="bg-bcgov-white dark:bg-bcgov-black inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full dark:text-white" + className="bg-bcgov-white dark:bg-bcgov-black inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full dark:text-white" >
@@ -59,25 +56,10 @@ export const DemoCompletedModal: React.FC = ({ action, cancel }) => {
- - Based on code by Animo + + Based on code by 4sure Technology Solutions
- - Business illustrations by Storyset - -
- Icons made by{' '} - - - Freepik - - {' '} - from{' '} - - Flaticon.com - -
diff --git a/apps/bc-wallet-demo-web/src/pages/dashboard/components/ProfileCard.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/ProfileCard.tsx new file mode 100644 index 000000000..a78381c10 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/ProfileCard.tsx @@ -0,0 +1,80 @@ +import React, { FC, useState } from 'react' +import { trackSelfDescribingEvent } from '@snowplow/browser-tracker' +import { motion } from 'framer-motion' +import { Modal } from '../../../components/Modal' +import { SmallButtonText } from '../../../components/SmallButtonText' +import { fade } from '../../../FramerAnimations' +import { useAppDispatch } from '../../../hooks/hooks' +import { showcaseServerBaseUrl } from '../../../api/BaseUrl' +import { basePath } from '../../../utils/BasePath'; +import { useNavigate } from 'react-router-dom'; +import type { Persona } from '../../../slices/types' +import { useShowcases } from '../../../slices/showcases/showcasesSelectors' +import { getTenantIdFromPath } from '../../../utils/Helpers' + +export interface Props { + currentPersona: Persona +} + +export const ProfileCard: FC = ({ currentPersona }) => { + const [isChangeModalOpen, setIsChangeModalOpen] = useState(false) + const dispatch = useAppDispatch() + const navigate = useNavigate() + const showcase = useShowcases() + const tenantId = getTenantIdFromPath(); + + const MODAL_TITLE = 'This will reset your dashboard.' + const MODAL_DESCRIPTION = `Your current credentials will become invalid. Please make sure you've completed all the use cases + before you do this.` + + const reset = () => { + trackSelfDescribingEvent({ + event: { + schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', + data: { + action: 'leave', + path: currentPersona.role, + step: 'dashboard', + }, + }, + }) + navigate(`${basePath}/${tenantId}/${showcase.showcase?.slug}`) + dispatch({ type: 'demo/RESET' }) + } + + const cancel = () => { + setIsChangeModalOpen(false) + } + + return ( +
+ + { currentPersona.bodyImage && + + } +

{currentPersona.name}

+ {/* FIXME we are using the onboarding step text here */} + {/*

*/} + {/* {currentPersona.description ??*/} + {/* currentCharacter?.onboarding.find((screen) => screen.screenId === 'PICK_CHARACTER')?.text}*/} + {/*

*/} +
+ setIsChangeModalOpen(true)} disabled={false} /> +
+ {isChangeModalOpen && ( + + )} +
+
+ ) +} diff --git a/client/src/pages/dashboard/components/RevocationContainer.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/RevocationContainer.tsx similarity index 96% rename from client/src/pages/dashboard/components/RevocationContainer.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/components/RevocationContainer.tsx index 962a0892a..736579599 100644 --- a/client/src/pages/dashboard/components/RevocationContainer.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/RevocationContainer.tsx @@ -1,12 +1,11 @@ -import type { CustomCharacter, RevocationInfoItem, RevocationRecord } from '../../../slices/types' +import React, { useState } from 'react' import { motion } from 'framer-motion' import { startCase } from 'lodash' -import React, { useState } from 'react' -import { dashboardTitle, rowContainer } from '../../../FramerAnimations' import { revokeCredential } from '../../../api/RevocationApi' - +import { dashboardTitle, rowContainer } from '../../../FramerAnimations' +import type { RevocationInfoItem, RevocationRecord } from '../../../slices/types' import { RevocationItem } from './RevocationItem' export interface Props { diff --git a/client/src/pages/dashboard/components/RevocationItem.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/RevocationItem.tsx similarity index 99% rename from client/src/pages/dashboard/components/RevocationItem.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/components/RevocationItem.tsx index 2441d063c..5a97e9b58 100644 --- a/client/src/pages/dashboard/components/RevocationItem.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/RevocationItem.tsx @@ -1,11 +1,10 @@ -import type { RevocationRecord } from '../../../slices/types' +import React from 'react' import { motion } from 'framer-motion' -import React from 'react' import { rowFadeX } from '../../../FramerAnimations' +import type { RevocationRecord } from '../../../slices/types' import { prependApiUrl } from '../../../utils/Url' - import { StartButton } from './StartButton' export interface Props { diff --git a/client/src/pages/dashboard/components/StartButton.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/StartButton.tsx similarity index 100% rename from client/src/pages/dashboard/components/StartButton.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/components/StartButton.tsx index 939c702f4..0611ddea2 100644 --- a/client/src/pages/dashboard/components/StartButton.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/StartButton.tsx @@ -1,11 +1,11 @@ import type { MouseEventHandler } from 'react' - -import { motion } from 'framer-motion' import React from 'react' import { FiCheck, FiLock } from 'react-icons/fi' -import { buttonHover } from '../../../FramerAnimations' +import { motion } from 'framer-motion' + import { Loader } from '../../../components/Loader' +import { buttonHover } from '../../../FramerAnimations' export interface Props { text: string diff --git a/apps/bc-wallet-demo-web/src/pages/dashboard/components/UseCaseContainer.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/UseCaseContainer.tsx new file mode 100644 index 000000000..af5aff0ca --- /dev/null +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/UseCaseContainer.tsx @@ -0,0 +1,57 @@ +import React, { FC } from 'react' +import { useNavigate } from 'react-router-dom' +import { motion } from 'framer-motion' +import { dashboardTitle, rowContainer } from '../../../FramerAnimations' +import { basePath } from '../../../utils/BasePath' +import { UseCaseItem } from './UseCaseItem' +import { StepActionType } from 'bc-wallet-openapi'; +import type { AriesOOBStepAction, Persona, Scenario, Showcase } from '../../../slices/types' +import { getTenantIdFromPath } from '../../../utils/Helpers' + +export interface Props { + showcase: Showcase + currentPersona: Persona + completedUseCaseSlugs: string[] + scenarios: Scenario[] +} + +export const UseCaseContainer: FC = ({ showcase, currentPersona, completedUseCaseSlugs, scenarios }) => { + const navigate = useNavigate() + const tenantId = getTenantIdFromPath(); + const startUseCase = (scenarioSlug: string) => { + navigate(`${basePath}/${tenantId}/${showcase.slug}/${currentPersona.slug}/presentations/${scenarioSlug}`) + } + + const renderUseCases = scenarios.map(scenario => { + const isCompleted = completedUseCaseSlugs.includes(scenario.slug) + const credentialDefinitions = scenario.steps.flatMap(step => + step.actions?.filter(action => action.actionType === StepActionType.AriesOob)?.flatMap(action => + (action as AriesOOBStepAction).credentialDefinitions ?? [] + ) ?? [] + ); + + return ( + + ) + }) + + return ( +
+ + Using your credentials + + + {renderUseCases} + +
+ ) +} diff --git a/client/src/pages/dashboard/components/UseCaseItem.tsx b/apps/bc-wallet-demo-web/src/pages/dashboard/components/UseCaseItem.tsx similarity index 69% rename from client/src/pages/dashboard/components/UseCaseItem.tsx rename to apps/bc-wallet-demo-web/src/pages/dashboard/components/UseCaseItem.tsx index ed2a49aa1..3c2a4f7be 100644 --- a/client/src/pages/dashboard/components/UseCaseItem.tsx +++ b/apps/bc-wallet-demo-web/src/pages/dashboard/components/UseCaseItem.tsx @@ -1,39 +1,20 @@ -import type { CustomCharacter } from '../../../slices/types' - +import React from 'react' import { trackSelfDescribingEvent } from '@snowplow/browser-tracker' import { motion } from 'framer-motion' import { startCase } from 'lodash' -import React from 'react' - import { rowFadeX } from '../../../FramerAnimations' -import { prependApiUrl } from '../../../utils/Url' - import { StartButton } from './StartButton' +import { showcaseServerBaseUrl } from '../../../api/BaseUrl'; +import type { CredentialDefinition, Persona } from '../../../slices/types' export interface Props { slug: string title: string - currentCharacter: CustomCharacter - requiredCredentials: string[] + currentPersona: Persona + requiredCredentials: CredentialDefinition[] isCompleted: boolean isLocked: boolean - start(slug: string): void -} - -const getCredIcon = (currChar: CustomCharacter, credName: string) => { - let icon = '' - currChar.useCases.forEach((useCase) => { - useCase.screens.forEach((screen) => { - if (screen) { - screen.requestOptions?.requestedCredentials.forEach((cred) => { - if (cred.name === credName) { - icon = cred.icon ?? '' - } - }) - } - }) - }) - return icon + start(scenarioSlug: string): void } export const UseCaseItem: React.FC = ({ @@ -43,7 +24,7 @@ export const UseCaseItem: React.FC = ({ requiredCredentials, isLocked, start, - currentCharacter, + currentPersona, }) => { return ( @@ -56,15 +37,15 @@ export const UseCaseItem: React.FC = ({

You'll be asked to share

- {requiredCredentials.map((item) => { + {requiredCredentials.map((requiredCredential) => { return ( -
+
credential icon -

{startCase(item)} 

+

{startCase(requiredCredential.name)} 

) })} @@ -76,7 +57,7 @@ export const UseCaseItem: React.FC = ({ schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', data: { action: 'start', - path: `${currentCharacter?.type.toLowerCase()}_${slug}`, + path: `${currentPersona.role.toLowerCase()}_${slug}`, step: 'usecase_start', }, }, diff --git a/client/src/pages/landing/LandingPage.tsx b/apps/bc-wallet-demo-web/src/pages/landing/LandingPage.tsx similarity index 100% rename from client/src/pages/landing/LandingPage.tsx rename to apps/bc-wallet-demo-web/src/pages/landing/LandingPage.tsx index 65add19a0..b0fc76ad0 100644 --- a/client/src/pages/landing/LandingPage.tsx +++ b/apps/bc-wallet-demo-web/src/pages/landing/LandingPage.tsx @@ -1,9 +1,9 @@ -import { motion } from 'framer-motion' import React from 'react' +import { motion } from 'framer-motion' + import { page } from '../../FramerAnimations' import { useTitle } from '../../hooks/useTitle' - import { Footer } from './components/Footer' import { MainSection } from './components/MainSection' import { NavBar } from './components/Navbar' diff --git a/client/src/pages/landing/components/Footer.tsx b/apps/bc-wallet-demo-web/src/pages/landing/components/Footer.tsx similarity index 100% rename from client/src/pages/landing/components/Footer.tsx rename to apps/bc-wallet-demo-web/src/pages/landing/components/Footer.tsx diff --git a/client/src/pages/landing/components/MainSection.tsx b/apps/bc-wallet-demo-web/src/pages/landing/components/MainSection.tsx similarity index 77% rename from client/src/pages/landing/components/MainSection.tsx rename to apps/bc-wallet-demo-web/src/pages/landing/components/MainSection.tsx index f699b1d9f..5b715c67c 100644 --- a/client/src/pages/landing/components/MainSection.tsx +++ b/apps/bc-wallet-demo-web/src/pages/landing/components/MainSection.tsx @@ -1,23 +1,16 @@ -import { trackSelfDescribingEvent } from '@snowplow/browser-tracker' -import { motion } from 'framer-motion' import React from 'react' import { isMobile } from 'react-device-detect' -import { FiExternalLink, FiArrowRight } from 'react-icons/fi' -import { useNavigate, useParams } from 'react-router-dom' +import { FiArrowRight, FiExternalLink } from 'react-icons/fi' +import { useNavigate } from 'react-router-dom' + +import { trackSelfDescribingEvent } from '@snowplow/browser-tracker' +import { motion } from 'framer-motion' -import { buttonHover, fade, fadeDelay, landingTitle } from '../../../FramerAnimations' import landingScreen from '../../../assets/light/landing-screen.svg' -import { useAppDispatch } from '../../../hooks/hooks' -import { useDarkMode } from '../../../hooks/useDarkMode' -import { setOnboardingStep } from '../../../slices/onboarding/onboardingSlice' +import { buttonHover, fade, fadeDelay, landingTitle } from '../../../FramerAnimations' import { basePath } from '../../../utils/BasePath' -import { addOnboardingProgress } from '../../../utils/OnboardingUtils' export const MainSection: React.FC = () => { - const darkMode = useDarkMode() - const dispatch = useAppDispatch() - const { slug } = useParams() - const navigate = useNavigate() const handleStart = () => { @@ -31,11 +24,7 @@ export const MainSection: React.FC = () => { }, }, }) - if (slug) { - navigate(`${basePath}/demo/${slug}`) - } else { - navigate(`${basePath}/demo`) - } + navigate(`${basePath}/${process.env.REACT_APP_DEFAULT_SLUG}`) } const renderMobileTitle = ( @@ -85,20 +74,21 @@ export const MainSection: React.FC = () => {
- - Get started   - - + {/* FIXME we need a tenant and showcase logic based on a tenant for this button */} + {/**/} + {/* Get started  */} + {/* */} + {/**/}

Get to know us  

diff --git a/client/src/pages/landing/components/Navbar.tsx b/apps/bc-wallet-demo-web/src/pages/landing/components/Navbar.tsx similarity index 100% rename from client/src/pages/landing/components/Navbar.tsx rename to apps/bc-wallet-demo-web/src/pages/landing/components/Navbar.tsx diff --git a/apps/bc-wallet-demo-web/src/pages/onboarding/OnboardingContainer.tsx b/apps/bc-wallet-demo-web/src/pages/onboarding/OnboardingContainer.tsx new file mode 100644 index 000000000..751166325 --- /dev/null +++ b/apps/bc-wallet-demo-web/src/pages/onboarding/OnboardingContainer.tsx @@ -0,0 +1,331 @@ +import type { FC, ReactElement } from 'react' +import React, { useEffect, useState } from 'react' +import { isMobile } from 'react-device-detect' +import { FiLogOut } from 'react-icons/fi' +import { useNavigate } from 'react-router-dom' +import { StepActionType } from 'bc-wallet-openapi' +import { trackSelfDescribingEvent } from '@snowplow/browser-tracker' +import { AnimatePresence, motion } from 'framer-motion' +import { showcaseServerBaseUrl } from '../../api/BaseUrl' +import { Modal } from '../../components/Modal' +import { fadeDelay, fadeExit } from '../../FramerAnimations' +import { useAppDispatch } from '../../hooks/hooks' +import { clearConnection } from '../../slices/connection/connectionSlice' +import { useCredentials } from '../../slices/credentials/credentialsSelectors' +import { clearCredentials } from '../../slices/credentials/credentialsSlice' +import { completeOnboarding, setScenario } from '../../slices/onboarding/onboardingSlice' +import { basePath } from '../../utils/BasePath' +import { getTenantIdFromPath, isConnected, isCredIssued } from '../../utils/Helpers' +import { setOnboardingProgress } from '../../utils/OnboardingUtils' +import { OnboardingBottomNav } from './components/OnboardingBottomNav' +import { PersonaContent } from './components/PersonaContent' +import { PickPersona } from './steps/PickPersona' +import { SetupCompleted } from './steps/SetupCompleted' +import { StepView } from './steps/StepView' +import type { + AcceptCredentialStepAction, + CredentialDefinition, + IssuanceScenario, + Persona, + Scenario, + Step +} from '../../slices/types' +import { useShowcases } from '../../slices/showcases/showcasesSelectors' +import { fetchWallets } from '../../slices/wallets/walletsThunks' + +export interface Props { + scenarios: Scenario[] + currentPersona?: Persona | null + connectionId?: string + connectionState?: string + invitationUrl?: string + currentStep?: Step +} + +export const OnboardingContainer: FC = ({ + scenarios, + currentPersona, + currentStep, + connectionId, + connectionState, + invitationUrl, +}) => { + const dispatch = useAppDispatch() + + useEffect(() => { + if (!sessionStorage.getItem('hasVisitedOnboarding')) { + dispatch({ type: 'demo/RESET' }) + sessionStorage.setItem('hasVisitedOnboarding', 'true') + } + }, [dispatch]) + + const { issuedCredentials } = useCredentials() + const [currentScenario, setCurrentScenario] = useState() + const [credentialDefinitions, setCredentialDefinitions] = useState([]) + const [credentialsAccepted, setCredentialsAccepted] = useState(false) + const [connectionCompleted, setConnectionCompleted] = useState(false) + const showcase = useShowcases() + const tenantId = getTenantIdFromPath(); + + const credDefParts = credentialDefinitions && credentialDefinitions[0]?.identifier?.split(':') + const credName = credDefParts && credDefParts[credDefParts.length - 1] + + useEffect((): void => { + if (!currentPersona && scenarios.length > 0) { + dispatch(setScenario(scenarios[0])) + } + }, [scenarios, currentPersona, dispatch]) + + useEffect((): void => { + setCurrentScenario(scenarios.find((scenario) => scenario.persona?.id === currentPersona?.id)) + }, [scenarios, currentPersona]) + + useEffect((): void => { + dispatch(setScenario(currentScenario)) + if (currentScenario) { + setOnboardingProgress(dispatch, currentScenario.steps[0]) + } + }, [currentScenario]) + + useEffect((): void => { + if (!currentStep?.actions?.some((action) => action.actionType === StepActionType.AcceptCredential)) { + return + } + + const credDefs = currentStep.actions.flatMap(action => (action as AcceptCredentialStepAction).credentialDefinitions ?? []); + setCredentialDefinitions(credDefs); + }, [currentStep]) + + useEffect((): void => { + setCredentialsAccepted(credentialDefinitions?.every((credentialDefinition: CredentialDefinition) => issuedCredentials.includes(credName))) + }, [credentialDefinitions, issuedCredentials]) + + useEffect((): void => { + if (!connectionState) { + setConnectionCompleted(false) + } else { + setConnectionCompleted(isConnected(connectionState)) + } + }, [connectionState]) + + useEffect(() => { + const handleTabClose = () => { + trackSelfDescribingEvent({ + event: { + schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', + data: { + action: 'leave_on_tab_close', + path: currentPersona?.role.toLowerCase(), + step: currentStep, + }, + }, + }) + dispatch({ type: 'demo/RESET' }) + } + + window.addEventListener('beforeunload', handleTabClose) + + return () => { + window.removeEventListener('beforeunload', handleTabClose) + } + }, [currentPersona, currentStep, dispatch]) + + const isBackDisabled: boolean = !currentStep || currentStep.order === 1 + const isForwardDisabled: boolean = (() => { + if (!currentStep) { + return true; + } + const hasSetupConnection = !!currentStep?.actions?.some((action) => action.actionType === StepActionType.SetupConnection); + if (hasSetupConnection && !connectionCompleted) { + return true; + } + const hasAcceptCredential = !!currentStep?.actions?.some((action) => action.actionType === StepActionType.AcceptCredential); + if (hasAcceptCredential && (!credentialsAccepted || credentialDefinitions.length === 0)) { + return true; + } + return false; + })() + + const nextOnboardingPage = async (): Promise => { + const nextStep = currentScenario?.steps[currentStep !== undefined ? currentStep.order : 0] + if (nextStep) { + trackSelfDescribingEvent({ + event: { + schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', + data: { + action: 'next', + path: currentPersona?.role.toLowerCase(), + step: currentStep, + }, + }, + }) + + setOnboardingProgress(dispatch, nextStep) + } + } + + const prevOnboardingPage = async (): Promise => { + const prevStep = currentStep && currentScenario?.steps[currentStep.order - 2] + if (prevStep) { + trackSelfDescribingEvent({ + event: { + schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', + data: { + action: 'back', + path: currentPersona?.role.toLowerCase(), + step: currentStep, + }, + }, + }) + + setOnboardingProgress(dispatch, prevStep) + } + } + + const skipGetCredential = async (): Promise => { + if (!currentStep || !currentScenario) { + return + } + + const nextIndex = currentScenario.steps + .slice(currentStep.order - 1) + .findIndex(step => + step.actions?.some(action => action.actionType === StepActionType.AcceptCredential) + ) + + if (!nextIndex || nextIndex === -1) { + return nextOnboardingPage() + } + + const target = nextIndex + currentStep.order + const nextStep = currentScenario.steps[target < currentScenario.steps.length ? target : currentStep.order] + trackSelfDescribingEvent({ + event: { + schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', + data: { + action: 'skip get credential', + path: currentPersona?.role.toLowerCase(), + step: currentStep, + }, + }, + }) + setOnboardingProgress(dispatch, nextStep) + } + + const getComponentToRender = (): ReactElement => { + if (!currentStep || currentStep.order === 1) { + return ( + scenario.persona)} + title={currentStep?.title} + text={currentStep?.description} + personaDescription={currentPersona?.description} + /> + ) + } else if (currentScenario?.steps.length === currentStep.order) { + return + } else { + return ( + + ) + } + } + + const getImageToRender = () => { + if (!currentStep || currentStep.order === 1) { + return + } else if (currentStep.asset) { + return ( + + ) + } + } + + const navigate = useNavigate() + const onboardingCompleted = () => { + if (connectionId && currentPersona) { + dispatch(clearCredentials()) + dispatch(clearConnection()) + dispatch(completeOnboarding()) + } else { + // something went wrong so reset + navigate(`${basePath}/`) + dispatch({ type: 'demo/RESET' }) + } + } + + const style = isMobile ? { minHeight: '85vh' } : { minHeight: '680px', height: '75vh', maxHeight: '940px' } + + const [leaveModal, setLeaveModal] = useState(false) + const LEAVE_MODAL_TITLE = 'Are you sure you want to leave?' + const LEAVE_MODAL_DESCRIPTION = `You're progress will be lost and you'll be redirected to the homepage.` + const showLeaveModal = () => setLeaveModal(true) + const closeLeave = () => setLeaveModal(false) + + const leave = () => { + trackSelfDescribingEvent({ + event: { + schema: 'iglu:ca.bc.gov.digital/action/jsonschema/1-0-0', + data: { + action: 'leave', + path: currentPersona?.role.toLowerCase(), + step: currentStep, + }, + }, + }) + navigate(`${basePath}/${tenantId}/${showcase.showcase?.slug}`) + dispatch(fetchWallets()) + dispatch({ type: 'demo/RESET' }) + } + + return ( + +
+
+ + + +
+ {getComponentToRender()} + +
+ {!isMobile && ( +
+ {getImageToRender()} +
+ )} + {leaveModal && ( + + )} +
+ ) +} diff --git a/apps/bc-wallet-demo-web/src/pages/onboarding/OnboardingPage.tsx b/apps/bc-wallet-demo-web/src/pages/onboarding/OnboardingPage.tsx new file mode 100644 index 000000000..f49b010eb --- /dev/null +++ b/apps/bc-wallet-demo-web/src/pages/onboarding/OnboardingPage.tsx @@ -0,0 +1,102 @@ +import React, { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' + +import { trackPageView } from '@snowplow/browser-tracker' +import { motion, AnimatePresence } from 'framer-motion' + +import { CustomUpload } from '../../components/CustomUpload' +import { page } from '../../FramerAnimations' +import { useAppDispatch } from '../../hooks/hooks' +import { useTitle } from '../../hooks/useTitle' +import { useConnection } from '../../slices/connection/connectionSelectors' +import { clearConnection } from '../../slices/connection/connectionSlice' +import { clearCredentials } from '../../slices/credentials/credentialsSlice' +import { useOnboarding } from '../../slices/onboarding/onboardingSelectors' +import { completeOnboarding } from '../../slices/onboarding/onboardingSlice' +import { usePreferences } from '../../slices/preferences/preferencesSelectors' +import { useShowcases } from '../../slices/showcases/showcasesSelectors' +import { clearShowcase } from '../../slices/showcases/showcasesSlice' +import { fetchShowcaseBySlug } from '../../slices/showcases/showcasesThunks' +import { fetchWallets } from '../../slices/wallets/walletsThunks' +import { basePath } from '../../utils/BasePath' +import { useSlug } from '../../utils/SlugUtils' +import { PageNotFound } from '../PageNotFound' +import { Stepper } from './components/Stepper' +import { OnboardingContainer } from './OnboardingContainer' +import { ScenarioType } from 'bc-wallet-openapi' +import { getTenantIdFromPath } from '../../utils/Helpers' + +export const OnboardingPage: React.FC = () => { + useTitle('Get Started | BC Wallet Self-Sovereign Identity Demo') + + const dispatch = useAppDispatch() + const navigate = useNavigate() + const { showcase, currentPersona } = useShowcases() + const slug = useSlug() + const { currentStep, isCompleted, scenario } = useOnboarding() + const { state, invitationUrl, id } = useConnection() + const { characterUploadEnabled } = usePreferences() + const tenantId = getTenantIdFromPath(); + + useEffect(() => { + if (isCompleted && showcase && currentPersona) { + dispatch(completeOnboarding()) + dispatch(clearCredentials()) + dispatch(clearConnection()) + navigate(`${basePath}/${tenantId}/${showcase.slug}/${currentPersona.slug}/presentations`) + } + }, [dispatch, slug, isCompleted, showcase, currentPersona]) + + useEffect(() => { + if (!showcase || showcase.slug !== slug) { + dispatch(clearShowcase()) + dispatch(fetchShowcaseBySlug(slug)) + } + }, [dispatch, slug, showcase]) + + useEffect(() => { + dispatch(fetchWallets()) + }, [dispatch]) + + useEffect(() => { + trackPageView() + }, []) + + if (showcase === undefined) { + dispatch(fetchShowcaseBySlug(slug)) + return
Loading...
+ } + + if (showcase === null) { + return + } + + return ( + <> + {characterUploadEnabled && } + + {scenario?.steps !== undefined && scenario?.steps.length > 0 && currentStep && ( + + )} + {showcase && ( + + scenario.type === ScenarioType.Issuance)} + currentPersona={currentPersona} + currentStep={currentStep} + connectionId={id} + connectionState={state} + invitationUrl={invitationUrl} + /> + + )} + + + ) +} diff --git a/client/src/pages/onboarding/components/Credential.tsx b/apps/bc-wallet-demo-web/src/pages/onboarding/components/Credential.tsx similarity index 100% rename from client/src/pages/onboarding/components/Credential.tsx rename to apps/bc-wallet-demo-web/src/pages/onboarding/components/Credential.tsx index 6fb88d813..3a12ecfbd 100644 --- a/client/src/pages/onboarding/components/Credential.tsx +++ b/apps/bc-wallet-demo-web/src/pages/onboarding/components/Credential.tsx @@ -1,11 +1,11 @@ -import type { CredentialData } from '../../../slices/types' +import React from 'react' import { motion } from 'framer-motion' import { startCase } from 'lodash' -import React from 'react' -import { fadeX } from '../../../FramerAnimations' import { StateIndicator } from '../../../components/StateIndicator' +import { fadeX } from '../../../FramerAnimations' +import type { CredentialData } from '../../../slices/types' import { isCredIssued } from '../../../utils/Helpers' import { prependApiUrl } from '../../../utils/Url' diff --git a/client/src/pages/onboarding/components/FailedRequestModal.tsx b/apps/bc-wallet-demo-web/src/pages/onboarding/components/FailedRequestModal.tsx similarity index 85% rename from client/src/pages/onboarding/components/FailedRequestModal.tsx rename to apps/bc-wallet-demo-web/src/pages/onboarding/components/FailedRequestModal.tsx index 0d578254c..ffb9326fe 100644 --- a/client/src/pages/onboarding/components/FailedRequestModal.tsx +++ b/apps/bc-wallet-demo-web/src/pages/onboarding/components/FailedRequestModal.tsx @@ -1,7 +1,7 @@ -import { AnimatePresence, motion } from 'framer-motion' +import { motion, AnimatePresence } from 'framer-motion' -import { standardFade, dropIn } from '../../../FramerAnimations' import { SmallButton } from '../../../components/SmallButton' +import { dropIn, standardFade } from '../../../FramerAnimations' export interface Props { action(): void @@ -26,7 +26,7 @@ export const FailedRequestModal: React.FC = ({ action, close, proof }) => >